воскресенье, 15 марта 2015 г.

GUI-testing 6. Thinking of testing №5

Original in Russian: http://18delphi.blogspot.ru/2013/11/5.html

GUI-testing. Table of contents

Now I want to explain “what is written there” and “why we need it”.
Actually, I wanted to give an example of how to test designed classes on our own, without DUnit (http://18delphi.blogspot.ru/2013/03/dunit_29.html), mock's (http://18delphi.blogspot.ru/2013/10/gui.html) and other (http://18delphi.blogspot.ru/2013/04/blog-post_8791.html) “odd staff”. .

I have been doing it for LONG TIME till I have mastered DUnit (http://18delphi.blogspot.ru/2013/03/blog-post.html).

How can we “simply take our designed class and test it” (http://18delphi.blogspot.ru/2013/04/tdd-delphi.html).

It should be noted – I am not looking for a “silver bullet”. As well as “spherical cows in vacuum”.

It should be clear that what I speak about is UNIVERSAL SOLUTIONS. They do NOT exist.

Once again – do NOT exist. UNIVERSAL SOLUTIONS.

If you start to look for “the disadvantages of the approach” and say “these were all particular cases”, I will note – YES, THESE ARE “particular cases”.

The WHOLE TESTING is made up of PARTICULAR CASES.

Some partial cases are derived from system requirements, some – from library specifications, some – from the already found errors.

These partial cases – EITHER WAY cover the tested system.

Absolute is a MYTH!

As well as a “silver bullet”.

But. “ANY boss-eyed and half-working, but WRITTEN test is BETTER than a million of PERFECT but UNWRITTEN ones”.

I want you to understand it.

If you still do not understand, it means the time FOR YOU to test has not yet arrived.

Probably, you “sleep in peace” as it is. The errors in the code do not bother you.

I’ll continue.

ANY test (I speak of) – AT LEAST allows you to determine the “regression” (https://en.wikipedia.org/wiki/Regression_testing), i.e. – to control the fact that in the system you develop when you change the code – NOTHING (tested already) gets out of order.

So.

I’ll try to analyze it all using the example.

First, I will ASSUME ONE THING.

We’ll analyze the testing of the DETERMINED code (https://en.wikipedia.org/wiki/Deterministic_algorithm) . on its “generalized parameters” (including, say, Self).

Let’s define the methods we’ll work with:

The particular object to be tested is TscriptParser.

Let its specification be as follows:

TscriptParser is a tool to parse the input symbol stream, which has to possess the following properties:

1. It parses the input stream for “tokens” (a sequence of “not empty characters”).
2. A “token” cannot move “between lines”. As “end-of-line” indicator the characters with codes 13 and 10 or their combination serve.
3. The empty characters are “space”, “tabbing” and (basing on the previous example) – characters with codes 13 and 10.
4. The “class” of tokens – “strings enclosed in single quotes” - is singled out specially. These tokens are featured by the “single quotes at the beginning” and the “single quotes at the end”.
5. Let our requirements specification (RS) keep secret of “double quotes” and “doubled single quotes”.
6. There are “commentaries” – ANY text in the file line beginning from the substring “//” and up to the end of the current line has to be IGNORED. It does not have to appear in the list of “tokens”.
7. Token does not have to be empty. It means token has to include at least ONE “not empty character”.

Actually, the given RS determines the code of TscriptParser, which is given in the previous series. (Within the quotes.)

And now I’ll tell how TscriptParser is organised:

I’ll note straight – this is NOT an EXAMPLE of how Parsers SHOULD BE written.

Once again – NOT an EXAMPLE of how you SHOULD write Parsers.

Were I not bound to the example, I would have written the code in absolutely different way. Using libraries, with internal buffer, address arithmetic and lambdas during parsing for “tokens”.

This refers to the fact that you don’t have to be “too captious” to the parser code.

Now, let’s see how it is organized.

It is made of “a number of layers”:
- GetChar – reads the character appearing in the input stream and gives message about file end if we reach it.
- ReadLn – is based on GetChar; reads the current line of the input stream from the current position to the file end or the “end-of-line” indicator. It also “preprocesses” the read line for “line comments” and deleting them.
- NextToken – is based on ReadLn and parsers the current line for tokens in accordance with the rules described above.

Now, let’s get to testing.

Previously I have written about the concept of “checkpoints” - in first chapter .

Later we’ll consider it.

Now, we’ll define the concepts we need for testing:

1. Test engine (TtestEngine) – an “engine” to determine the state of the tested system.
2. Tested system – the code that we test using test engine (unfortunately, the tautology is inevitable). In our case the tested system is our class TscriptParser.
3. Experiment or test (TtestInstance) - a particular experiment of the tested system for FIXED input parameters. Let’s assume that experiments can’t be nested. It means, all code of tests we describe can carry out only ONE experiment at the interval of [t1..t2].
4. “Checkpoint” or “connection socket” (TtestSocket) – NAMED (identifiable, differentiated from the similar ones) object describing the special code fragment of the tested system.
5. “Measured value” (TtestValue) – a particular value taken from the “connection socket” at the moment.
6. Socket metric (TtestSocketMetric) – an array of “measured values” (TtestValue and TtestMetricValues) taken from the SPECIFIC socket during the WHOLE experiment.

Let us think that the methods of classes TscriptParser - GetChar, ReadLn and NextToken described above are the IMPORTANT spots of the tested system for taking readings using test engine.

I.e. these spots are the “checkpoints”.

How it looks in code:

The test engine and the associated concepts look like this:

unit Testing.Engine;
 
interface
 
{$IfNDef NoTesting}
uses
 System.Classes,
 Core.Obj
 ;
 
type
 TtestSocket = record
  {* - checkpoint }
  private
   rClass: TClass;
   rMethod: String;
  public
   constructor Create(anObject: TObject; const aMethod: String);
    {* - Creates a copy of checkpoint. }
   function EQ(const anOther: TtestSocket): Boolean;
    {* - Determines that checkpoints are the same }
 end;//TtestSocket
 
 TtestValueType = (test_vtChar, test_vtString);
  {* - Possible types of the measured values }
 
 TtestValue = record
  {* - The measured value }
  private
   rType : TtestValueType;
    {* - The type of the measured value. }
   rAsString : AnsiString;
   rAsChar : AnsiChar;
  public
   constructor CreateAsChar(aChar: AnsiChar);
   constructor CreateAsString(const aString: AnsiString);
 end;//TtestValue
 
 TtestMetricValues = record
  {* - The array of the values for metric. }
  private
   f_Stream : TStream;
    {* The place to write the values of metrics. }
  public
   procedure Init(const aTestName: String; const aSocket: TtestSocket);
    {* - Initializes the array of the values for metric }
   procedure FlushAndClear;
    {* - Writes the array to a durable medium and spares/clears it. }
   procedure PutValue(const aValue: TtestValue);
    {* - Writes the current value to the array of values for metrics }
 end;//TtestMetricValues
 
 TtestMetric = record
  {* - The measured metrics. }
  private
   rSocket : TtestSocket;
    {* - The checkpoint to take metrics }
   rValues : TtestMetricValues;
    {* - The current array of values taken from checkpoint }
  public
   constructor Create(const aTestName: String; const aSocket: TtestSocket);
   function EQ(const anOther: TtestSocket): Boolean;
    {* - Checks if metrics is taken from a specific checkpoint }
   procedure FlushAndClear;
    {* - Writes the array to a durable medium and spares/clears it. }
   procedure PutValue(const aValue: TtestValue); overload;
    {* - Writes the current value to the array of values for metrics }
   procedure PutValue(const aValue: String); overload;
    {* - Writes the current value to the array of values for metrics }
   procedure PutValue(const aValue: AnsiChar); overload;
    {* - Writes the current value to the array of values for metrics }
 end;//TtestMetric
 PtestMetric = ^TtestMetric;
 
 TtestMetrics = array of TtestMetric;
  {* - ALL metrics taken in the PARTICULAR experiment }
 
 TtestInstance = class(TCoreObject)
  {* - Experiment }
  private
   f_TestName : String;
   f_Metrics : TtestMetrics;
  protected
   procedure Cleanup; override;
  public
   constructor Create(const aTestName: String);
   function SocketMetric(const aSocket: TtestSocket): PtestMetric;
    {* - Returns a copy of current metrics for the specific “checkpoint” to continue working with it }
 end;//TtestInstance
 
 TtestEngine = class
  {* - Engine for testing }
  public
   class function StartTest(const aTestName: String): TtestInstance;
    {* - Launches the test (experiment) }
   class procedure StopTest;
    {* - Completes the current experiment }
   class function CurrentTest: TtestInstance;
    {* - The currently carried out experiment }
 end;
{$EndIf NoTesting}
 
implementation
 
{$IfNDef NoTesting}
 
uses
 System.SysUtils
 ;
 
var
 g_CurrentTest : TtestInstance = nil;
 
constructor TtestValue.CreateAsChar(aChar: AnsiChar);
begin
 inherited;
 rType := test_vtChar;
 rAsChar := aChar;
end;
 
constructor TtestValue.CreateAsString(const aString: AnsiString);
begin
 inherited;
 rType := test_vtString;
 rAsString := aString;
end;
 
constructor TtestSocket.Create(anObject: TObject; const aMethod: String);
begin
 inherited;
 rClass := anObject.ClassType;
 rMethod := aMethod;
end;
 
function TtestSocket.EQ(const anOther: TtestSocket): Boolean;
 {* - Determines that checkpoints are the same }
begin
 Result := (Self.rClass = anOther.rClass) AND (Self.rMethod = anOther.rMethod);
end;
 
constructor TtestMetric.Create(const aTestName: String; const aSocket: TtestSocket);
begin
 inherited;
 rSocket := aSocket;
 rValues.Init(aTestName, aSocket);
end;
 
function TtestMetric.EQ(const anOther: TtestSocket): Boolean;
 {* - Checks if metrics is taken from the specific checkpoint }
begin
 Result := Self.rSocket.EQ(anOther);
end;
 
procedure TtestMetric.FlushAndClear;
 {* - Writes the array to a durable medium and spares/clears it. }
begin
 Self.rValues.FlushAndClear;
end;
 
procedure TtestMetric.PutValue(const aValue: TtestValue);
 {* - Writes the current value to the array of values for metrics }
begin
 Self.rValues.PutValue(aValue);
end;
 
procedure TtestMetric.PutValue(const aValue: String);
 {* - Writes the current value to the array of values for metrics }
begin
 Self.rValues.PutValue(TtestValue.CreateAsString(aValue));
end;
 
procedure  TtestMetric.PutValue(const aValue: AnsiChar);
 {* - Writes the current value to the array of values for metrics }
begin
 Self.rValues.PutValue(TtestValue.CreateAsChar(aValue));
end;
 
procedure TtestMetricValues.Init(const aTestName: String; const aSocket: TtestSocket);
 {* - Initializes the array of values for metrics }
begin
 f_Stream := TFileStream.Create(ExtractFilePath(ParamStr(0)) + '\' +
                                ExtractFileName(aTestName) +
                                '.' + aSocket.rClass.ClassName +
                                '.' + aSocket.rMethod +
                                '.out', fmCreate);
end;
 
procedure TtestMetricValues.FlushAndClear;
 {* - Writes the array to a durable medium and spares/clears it. }
begin
 FreeAndNil(f_Stream);
end;
 
procedure TtestMetricValues.PutValue(const aValue: TtestValue);
 {* - Writes the current value to the array of values for metrics }
const
 cEOL : AnsiString = #13#10;
begin
 Assert(f_Stream <> nil, 'The file to write the values of metrics is not open');
 case aValue.rType of
  test_vtChar:
   f_Stream.Write(@aValue.rAsChar, SizeOf(aValue.rAsChar));
  test_vtString:
  begin
   f_Stream.Write(@aValue.rAsString[1], Length(aValue.rAsString));
   f_Stream.Write(@cEOL[1], Length(cEOL));
  end;//test_vtString
  else
   Assert(false, 'Unknown type of metrics values');
 end;
end;
 
constructor TtestInstance.Create(const aTestName: String);
begin
 Assert(g_CurrentTest = nil, 'Nested experiments are not supported');
 f_TestName := aTestName;
 inherited Create;
 g_CurrentTest := Self;
end;
 
procedure TtestInstance.Cleanup;
var
 l_Index : Integer;
begin
 for l_Index := Low(f_Metrics) to High(f_Metrics) do
  f_Metrics[l_Index].FlushAndClear;
 Finalize(f_Metrics);
 inherited;
end;
 
function TtestInstance.SocketMetric(const aSocket: TtestSocket): PtestMetric;
 {* - Returns a copy of the current metrics for the specific “checkpoint” to continue working with it }
var
 l_Index : Integer;
begin
 for l_Index := Low(f_Metrics) to High(f_Metrics) do
  if f_Metrics[l_Index].EQ(aSocket) then
  begin
   Result := @f_Metrics[l_Index];
   Exit;
  end;//f_Metrics[l_Index].EQ(aSocket)
 SetLength(f_Metrics, Succ(Length(f_Metrics)));
 f_Metrics[High(f_Metrics)] := TtestMetric.Create(f_TestName, aSocket);
 Result := @f_Metrics[High(f_Metrics)];
end;
 
class function TtestEngine.StartTest(const aTestName: String): TtestInstance;
 {* - Launches the test (experiment)}
begin
 Result := TtestInstance.Create(aTestName);
end;
 
class procedure TtestEngine.StopTest;
 {* - Completes the current experiment }
begin
 Assert(g_CurrentTest <> nil, 'Something has gone wrong. No current experiment');
 FreeAndNil(g_CurrentTest);
end;
 
class function TtestEngine.CurrentTest: TtestInstance;
 {* - Currently carried out experiment}
begin
 Result := g_CurrentTest;
end;
{$EndIf  NoTesting}
 
end.

The code of parser and calls of test engines look like this:

unit Script.Parser;
 
interface
 
uses
 Classes,
 Core.Obj
 ;
 
{$IfNDef NoTesting}
 {$Define TestParser}
{$EndIf  NoTesting}
 
type
 TscriptTokenType = (script_ttUnknown, script_ttToken, script_ttString);
 
 TscriptParser = class(TCoreObject)
  private
   f_Stream : TStream;
   f_EOF : Boolean;
   f_CurrentLine : String;
   f_PosInCurrentLine : Integer;
   f_Token : String;
   f_TokenType : TscriptTokenType;
  protected
   procedure Cleanup; override;
   function ReadLn: String;
  protected
   function GetChar(out aChar: AnsiChar): Boolean;
  public
   constructor Create(const aStream : TStream); overload;
   constructor Create(const aFileName : String); overload;
   function EOF: Boolean;
   procedure NextToken;
  public
   property TokenString: String
    read f_Token;
 end;//TscriptParser
 
implementation
 
uses
 System.SysUtils
 {$IfDef TestParser}
 ,
 Testing.Engine
 {$EndIf TestParser}
 ;
 
constructor TscriptParser.Create(const aStream : TStream);
begin
 inherited Create;
 f_PosInCurrentLine := 1;
 f_EOF := false;
 f_Stream := aStream;
end;
 
constructor TscriptParser.Create(const aFileName : String);
var
 l_FileName : String;
begin
 l_FileName := ExtractFilePath(ParamStr(0)) + '\' + aFileName;
 Create(TFileStream.Create(l_FileName, fmOpenRead));
end;
 
procedure TscriptParser.Cleanup;
begin
 FreeAndNil(f_Stream);
 inherited;
end;
 
function TscriptParser.GetChar(out aChar: AnsiChar): Boolean;
begin
 if (f_Stream.Read(aChar, SizeOf(aChar)) = SizeOf(aChar)) then
 begin
  Result := true;
  {$IfDef TestParser}
  TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'GetChar')).PutValue(aChar);
  // - we take readings of the current checkpoint
  {$EndIf TestParser}
 end
 else
  Result := false;
end;
 
function TscriptParser.ReadLn: String;
{$IfDef TestParser}
var
 l_Result : AnsiString;
{$EndIf TestParser}
var
 l_Char : AnsiChar;
 l_Line : String;
 l_LineCommentPos : Integer;
begin
 {$IfDef TestParser}
 try
 {$EndIf TestParser}
  try
   l_Line := '';
   while GetChar(l_Char) do
   begin
    if (l_Char = #13) then
    begin
     if GetChar(l_Char) then
     begin
      if (l_Char = #10) then
      begin
       Result := l_Line;
       Exit;
      end//l_Char = #10
      else
       Assert(false, 'Something has gone wrong, there’s no character 10 after 13');
     end//GetChar(l_Char)
     else
      Assert(false, 'Something has gone wrong, at once the file end after character 13');
    end;//l_Char = #13
    l_Line := l_Line + l_Char;
   end;//while GetChar(l_Char)
   f_EOF := true;
   Result := l_Line;
  finally
   l_LineCommentPos := Pos('//', Result);
   if (l_LineCommentPos > 0) then
   begin
    Delete(Result, l_LineCommentPos, Length(Result) - l_LineCommentPos + 1);
   end;//l_LineCommentPos > 0
  end;//try..finally
 {$IfDef TestParser}
 finally
  TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'ReadLn')).PutValue(Result);
  // - we take readings of the current checkpoint
 end;//try..finally
 {$EndIf TestParser}
end;
 
procedure TscriptParser.NextToken;
const
 cQuote = #39;
 cWhiteSpace = [#32,#9];
begin
 f_TokenType := script_ttUnknown;
 f_Token := '';
 while true do
 begin
  if (f_PosInCurrentLine >= Length(f_CurrentLine)) then
  begin
   // - ALL current line kind of has been processed
   f_CurrentLine := '';
   f_PosInCurrentLine := 1;
  end;//f_PosInCurrentLine > Length(f_CurrentLine)
  while(f_CurrentLine = '') do
  begin
   f_CurrentLine := ReadLn;
   if (f_CurrentLine = '') then
    if f_EOF then
     Exit;
  end;//while(f_NextToken = '')
  // Here we pass empty characters:
  while (f_PosInCurrentLine <= Length(f_CurrentLine)) do
   if (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace) then
    Inc(f_PosInCurrentLine)
   else
    break;
  if (f_PosInCurrentLine <= Length(f_CurrentLine)) then
   break;
 end;//while true
 
 // Here we collect NOT empty characters:
 if (f_CurrentLine[f_PosInCurrentLine] = cQuote) then
 begin
  f_TokenType := script_ttString;
  Inc(f_PosInCurrentLine);
  while (f_PosInCurrentLine <= Length(f_CurrentLine)) do
   if (f_CurrentLine[f_PosInCurrentLine] <> cQuote) then
   begin
    f_Token := f_Token + f_CurrentLine[f_PosInCurrentLine];
    Inc(f_PosInCurrentLine);
   end//not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace)
   else
    break;
 end//f_CurrentLine[f_PosInCurrentLine] = ''
 else
 begin
  f_TokenType := script_ttToken;
  while (f_PosInCurrentLine <= Length(f_CurrentLine)) do
   if (not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace)) then
   begin
    f_Token := f_Token + f_CurrentLine[f_PosInCurrentLine];
    Inc(f_PosInCurrentLine);
   end//not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace)
   else
    break;
 end;//else
 {$IfDef TestParser}
 case f_TokenType of
  script_ttString:
   TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'NextToken')).PutValue('Single quoted string:');
  script_ttToken:
   // - we do nothing
   ;
  else
   Assert(false, 'Something has gone wrong');
 end;//case f_TokenType
 TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'NextToken')).PutValue(f_Token);
 // - we take readings of the current checkpoint
 {$EndIf TestParser}
 //f_CurrentLine := '';
end;
 
function TscriptParser.EOF: Boolean;
begin
 Result := f_EOF AND (f_CurrentLine = '');
end;
 
end.

Finally, the call of parser in the test wrapping looks like this:

var
 l_Parser : TscriptParser;
begin
 TtestEngine.StartTest(aFileName);
 try
  l_Parser := TscriptParser.Create(aFileName);
  try
   while not l_Parser.EOF do
    l_Parser.NextToken;
  finally
   FreeAndNil(l_Parser);
  end;//try..finally
 finally
  TtestEngine.StopTest;
 end;//try..finally
end;

The whole code of the example is here - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/

Actually, the test “script” for parser - FirstScript.script looks like this:

DoNothing // - for now we do nothing
 
// - here we do nothing too for now
DoNothing1 DoNothing2 // - here we do nothing too for now, but we check TWO tokens
DoNothing3 DoNothing4 // - here we do nothing too for now, but we check TWO tokens with a spaces at the BEGINNING
DoNothing5 DoNothing6 // - here we do nothing too for now, but we check TWO tokens without spaces in the END
DoNothing7 // - here we do nothing too for now, but we check ONE token without spaces in the END
DoNothing8 DoNothing9 DoNothing10 // - here we do nothing too for now, but we check THREE tokens
DoNothing11 DoNothing12 DoNothing13 DoNothing14 // - here we do nothing too for now, but we check FOUR tokens
   // - we process a “purely empty line”
'aString1'// - we try to process a string 
'aString2 with spaces'// - we try to process a string with spaces
   // - we process a “purely empty line”

P.S. I’d like to tell to Delphi ANTAGONISTS – GUYS, this is not about Delphi. Generally speaking.

If you wish, I’ll write THE SAME on Obj-C or Python.

P.P.S. This post is IMHO the BEST I’ve ever written about “work”. If it is “not interesting” or “not understandable” – then, probably, I should stop “writing”. Probably, I can’t clearly make a point of “what I’d like to clear”. I am not able to run after Bagel or GunSmoker. I write about other things. About the APPROACH to developing… It’s a pity if you don’t understand.

P.P.P.S. I’m ready to bet... If some “of my posts” was written by “some” Fowler or Larman, they would say – “that’s a miracle! The truth is now clear for us..."

My respect to Bagel and Gunsmoker.

P.P.P.P.S. A good man suggested this theme is GREATLY related to Dependency Injection (https://en.wikipedia.org/wiki/Dependency_injection) and gave me the reference - http://docs.jboss.org/weld/reference/1.0.0/en-US/html/


NO DOUBT I will try to discover this theme.

P.P.P.P.P.S. By the way, it is most simple to do Dependency Injection on Objective-C or Python. There are no private methods (all methods are somehow available by name) and one can “override class to COPY method” instead of the whole class in general.

P.P.P.P.P.P.S. The example was compiled and tested on Delphi XE4 and Delphi XE5.

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

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