среда, 18 марта 2015 г.

Testing of calculator №6.2.2. Processing of false data and boundary conditions

Original in Russian: http://programmingmindstream.blogspot.com/2014/06/622.html

Table of contents

I should just write about the nights without calls and there they were. The customer sent the picture:


In short, naturally, I forgot about division by zero.
Surprised, I asked: “Don’t you know one can not divide by zero?”
The customer claimed “The calculator has to give an answer: “Division by 0”.

It was night, I found a quick solution – I just checked the denominator and got the answer “Division by 0”.

However, in the morning I had to put our tests right and consider the errors of division by 0. What is the reason? We’ll analyze it in this article.
Alexander offered to follow a plan, and here it is:
1. We fix an error. ONE error.

class function TCalculator.Divide(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  if x2=0 then
  begin
    Result := 'Division by 0';
    exit;
  end;
  x3 := x1 / x2;
  Result := FloatToStr(x3);
end;

2. We write a test to check the logic:

procedure TCalculatorOperationViaLogicTest.TestZeroDivide;
var
  x1, x2: string;
begin
  x1:= cA;
  x2:= '0';
  CheckTrue(c_ZeroDivideMessageError = TCalculator.Divide(x1, x2));
end;

3. We make a test with etalons (hereinafter when we write ETALONS we always mean ETALONS based on pseudo-random data), in which false input data takes part. First, I’d like to give the code of the procedure forming “pseudo-random” sequence, that works now:

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
 aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;

As you can see, the second argument we pass as a denominator will surely never equal zero. We’ve done this because the customer has not written it in requirements specification on purpose, so that to eliminate the errors at the early stages of testing. The time has come to change it. We take away +1.

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;

We've launched the tests and, of course, all etalons have failed. One more thing is interesting. The exception with division by 0 has been triggered. It is strange since probability was not very high. Later on, I'll explain the situation.


4. We ensure it to do what it’s intended to.
We delete our etalons since the second operator for tests has been changed.


5. We found the SECOND error in DivInt. Using the previous test.
As you can see, the tests have passed well (i.e. the ETALONS were created one more time), but there’s the exception of division by 0 on launch of DivInt left.

6. We turn the call of logic from test to block try..except. In try..except we write Exception.ClassName to etalon instead of result. It is natural for false data.
We change the code:

procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
end;

Into:

procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  try
    aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
  except
    on E : Exception do
      aLogger.ToLog(E.ClassName);
  end;
end;

Therefore, our tests are all “green”, although on “launch and debugging” the Exception (mentioned above) is raised.


When I got into etalons to see what was wrong I remembered about requirements specifications.

TCalculatorOperationRandomSequenceTestTestDivInt.etalon
...
277.833182131872
0.400131568312645
EDivByZero 
...

Do you remember I wrote in the previous chapter that the customer didn’t tell us what to do with real numbers in DivInt operation? There are the results. I’ll remind you the code to solve the problem with real numbers:

class function TCalculator.DivInt(const A, B: string): string;
var
  x1, x2, x3  : Integer;
begin
  x1 := round(StrToFloat(A));
  x2 := round(StrToFloat(B));
  x3 := x1 div x2;
  Result := FloatToStr(x3);
end;

Since we round numbers to integers, the exception with division by 0 is raised. We change the code into this:

class function TCalculator.DivInt(const A, B: string): string;
var
  x1, x2, x3  : Integer;
begin
  x1 := round(StrToFloat(A));
  x2 := round(StrToFloat(B));
  try
    x3 := x1 div x2;
  except
    on EDivByZero do
    begin
      Result:= c_ZeroDivideMessageError;
      Exit;
    end;
  end;
  Result := FloatToStr(x3);
end;

After launching tests we’ll have to delete the etalon for DivInt because the business logic of the application has been changed. I’d like to emphasize the difference in processing exceptions in business logic and in testing. In business logic we check “division by 0” and display the result, i.e. tailor to hit the client’s specifications. On the contrary, in testing we check all versions and write the relevant exception to “etalon” file. In the coming chapters I’ll tell why we do in this exact way. Now I’ll just wish you to imagine how an application would “behave” itself if we added numbers that exceed “extended”...

7. We write the test of DivInt logic.
We had a debate with Alexander on this item about what is initial – testing of logic or launching of Random etalons with a probability of 1 to 10000 as for an error to show up. But this is a subject for another article :).

The commentary by Alexander:
“They say” – “two LAWYERS – three judgments”, and it’s more funny with programmers: “two PROGRAMMERS – five and a half judgments :-)”)

So, let’s write test of logic for division by 0 for DivInt:

procedure TCalculatorOperationViaLogicTest.TestZeroDivInt;
var
  x1, x2: string;
begin
  x1:= cA;
  x2:= '0';
  CheckTrue(c_ZeroDivideMessageError = TCalculator.DivInt(x1, x2));
end;

We launch the tests and see it is all ok since we’ve corrected the logic at a previous step.



As you can see, we do not need the rest of the steps because we’ve taken them previously.


8. WE CORRECT the SECOND error. In DivInt.
9. We run tests with etalons. They do not coincide for DivInt.
10. We create the etalons again.

There’s one last step – checking of our operation for division with a denominator 0 explicitly. The simplest way of doing it is to specify each 100th number as 0 explicitly.

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
  x1, x2 : single;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
  begin
    x1 := 1000 * Random;
    x2 := 2000 * Random;
    if (l_Index mod 100) = 0 then
      x2 := 0;
    CheckOperation(aLogger,
                   x1,
                   x2, anOperation);
  end;
  CheckTrue(aLogger.CheckWithEtalon);
end;

We launch:


As you can see, all our etalons failed again. We delete the old ones and rerun the tests, the results are merged to git. At the same time, we rewrite our solution made in the night for exceptions check:

class function TCalculator.Divide(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  try
    x3 := x1 / x2;
  except
    on EDivByZero do
    begin
      Result:= c_ZeroDivideMessageError;
      Exit;
    end;
  end;
  x3 := x1 / x2;
  Result := FloatToStr(x3);
end;

At this point, I’d like to finish the post. But, my dialogs with Alexander developed a run-on I could not have noticed myself.

Since we check exceptions twice and define in the business logic only the exception that we are “aware of”, it would be good if at the end of (Etalon-Random) testing we check if there’s no exception raised.

Due to the tests architecture, there will be a small amount of code. We change the procedure of “sequence” launch:

procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
var
  l_ExceptionCount: integer;
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  l_ExceptionCount:= 0;
  try
    aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
  except
    on E : Exception do
    begin
      aLogger.ToLog(E.ClassName);
      inc(l_ExceptionCount);
    end;
  end;
  Check(l_ExceptionCount = 0);
end;

Looking forward to seeing “all lamps are green”, I've been very surprised to see the violet one (the test has failed) and the red one (the exception has been raised).


I did not get into debug. I have begun to think why this happened. At a core, we’ve finished everything. First, I “put blame” on my last change (Special thanks to people who developed GIT and other versions control systems). I've rolled back to commit… Having launched the tests, I’ve seen the same.

This very moment is very significant. After I have changed comparing of denominator with 0 (solution made in the night) for exceptions checking and have received the expected result, I have decided that “the job is in the bag”. I’ve committed the code and have planned to finish the article.
What is my chief error?
I haven’t used the advantages of the approach I describe. TESTS CHECK the WORK of the programmer. I didn’t launch tests because I decided “there’s surely all fine”.
Our 2 tests failed:
- Test of division by 0 logic.
- Test of etalons with 10к versions.

The first test of logic of “checking division by 0”:

procedure TCalculatorOperationViaLogicTest.TestZeroDiv;
var
  x1, x2: string;
begin
  x1:= cA;
  x2:= '0';
  CheckTrue(c_ZeroDivideMessageError = TCalculator.Divide(x1, x2));
end;

That is the error number one. The constant c_ZeroDivideMessageError we need will only appear when we’ll have Exception for integer division by 0, or EDivByZero http://docwiki.embarcadero.com/Libraries/XE3/en/System.SysUtils.EDivByZero. Instead, we catch EZeroDivide http://docwiki.embarcadero.com/Libraries/XE3/en/System.SysUtils.EZeroDivide.
There are such types of exceptions in Delphi, so a new class has been introduced for real numbers. Generally, I understand what for. But my tests have not. We will change.
The second test has failed because it hasn’t coincided with the etalon, in which a string value of the constant c_ZeroDivideMessageError has already been written.

Since only real numbers get in Div operation, we change exceptions check for the required type:

class function TCalculator.Divide(const A, B: string): string;
var
  x1, x2, x3 : single;
begin
  x1 := StrToFloat(A);
  x2 := StrToFloat(B);
  try
    x3 := x1 / x2;
  except
    on EZeroDivide do
    begin
      Result:= c_ZeroDivideMessageError;
      Exit;
    end;
  end;
  x3 := x1 / x2;
  Result := FloatToStr(x3);
end;

It’s all fine:


But!!!

Our check of exceptions number is currently in CheckOperation procedure. So, each time we launch operation for testing, we check if there is no exception. For now, it is what we need. On the other hand, the point of check is lost. We change it:

procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index, l_ExceptionCount : Integer;
  x1, x2 : single;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
 
  l_ExceptionCount:= 0;
  for l_Index := 0 to 10000 do
  begin
    try
      x1 := 1000 * Random;
      x2 := 2000 * Random;
      if (l_Index mod 100) = 0 then
        x2 := 0;
      CheckOperation(aLogger,
                     x1,
                     x2, anOperation);
      except
        on E : Exception do
        begin
          aLogger.ToLog(E.ClassName);
          inc(l_ExceptionCount);
        end;
    end;
    end;
  CheckTrue(aLogger.CheckWithEtalon);
  Check(l_ExceptionCount = 0);
end;

We launch tests and MAKE SURE it is all ok.

Let’s sum up.
First of all, RS, RS and RS. Thinking and making clear is our job, but not the client’s.
Every time you change the logic or testing conditions you update the etalons.
Every time you CHANGE ANYTHING – YOU CHECK YOURSELF USING TESTS.
Otherwise, why do we actually need tests ?
Zero does not always equal 0, it’s so happened in programming, but that’s a subject for a special article.

Check for exceptions number allows us be more confident about “thickness of walls” against errors. Still, the man sitting at the keyboard is the key. He can always make a mistake, at least he can forget to launch tests as I did.
Programming is a complex and in some way unpredictable process. Moreover, we are all humans. We all make mistakes. Although creating extra “strengthening of defense” produces man-hour expenses, instead we get QUALITY and PRECAUTION. However, one should not forget about contradictions in RS and so on. We’ll discuss it some day in the future.

p.s. As I’ve been preparing the article to be published I have chanced upon the great article of GunSmoker about expanding Exception class after Delphi 2009 http://www.gunsmoker.ru/2010/04/exception-delphi-2009.html.

Repository

UML-diagram of the classes:



Комментариев нет:

Отправить комментарий