XIDE, NUNIT and functions.
Posted: Wed Oct 05, 2016 8:38 pm
Hi pearlers of X#,
for all of you coming directly from VO to X#, I will introduce you to something that was not possible with VO: Unit Testing.
For more detailed information I strongly recommend these two books:
Working Effectivly with Legacy Code by Micheal C. Feathers
and
The Art of Unit Testing, by Roy Osherove
Both are available in german, so I think, they are available in many other languages too.
More books to read may be found here:
http://www.codeproject.com/Reference/61 ... ence-Books
Now, what is Unit Testing? In short words:
- It's the first defense line against bugs!
and
- it prevents in many cases the use of a debugger. I feel, and since I do unit testing more than ever, that debugging is mostly a waste of time.
The biggest con: it cannot be repeated automaticly. We have to do it manual. It's like shooting in the dark.
Unit testing is more like engineering, is quality control, beginning from the first line of code to the last one. And at the ends it saves the value of your code base.
Software without unit tests is effectivly unsaleable! Who wants to buy a pig in a poke? And what prevents against unwanted side effects of code changes.
Now, as always, the first step is the hardest. But I promise you, as soon as you have found your first hidden bug, you never will miss the tests. For me it took only one hour.
I do unit testing with XIDE and NUNIT. http://www.nunit.org/
For now I recommend to download and install V 2.6. The simple reason: Until today they do not have a GUI runner for V 3, which I like to use. But be careful, don't use features that are flagged deprecated.
Then we do some settings in XIDE.
First we create a new project configuration. I name it NUNIT. Project->Properties->Configuration->Add
Now we create a new application.
I recommend as a general tip to set one common output directory for all applications of a project. In fact I use the same directory for all my projects.
Application->Properties->General->Output folder:
For example I name it D:InstallSource/Assemblies
We copy the NUnit framework assembly nunit.framework.dll into this directory.
At Application->Properties->General->Compiler->Build configurations->NUnit we insert
/define:NUNIT
At Application->Properties->References->Browse we click 'Browse disk for dll files' and search and select nunit.framework.dll .
The program and the corresponding tests are put into two separate PRG's. For example
Functions.prg and FunctionsTests.prg
This way they are always grouped together.
First I show the general frame, where the business code and the test code is in. My example is a function, where the class is a static class, that cannot be instantiated.
But the schema is always the same.
// Functions.prg, contains the working code, that has to be tested.
PARTIAL STATIC CLASS Funktionen // important: PARTIAL!
...
END CLASS
// FunktionenTests.prg, contains the tests
#ifdef NUNIT // Compile only, if define NUNIT exists, see project configuration and application setting.
#using NUnit.Framework
PARTIAL STATIC CLASS Funktionen // Seems to be senseless. But through using the 'Nested Class' construct allowas the tests access to private members tested class!
[TestFixture] ;
SEALED CLASS FunktionsTests INHERIT AssertionHelper // AssertionHelper is a NUnit class.
...
END CLASS
END CLASS
#endif
I think, this is easier to recognize then embedded in ful example. This frame can be saved as a code snippet.
After the first compile (complete example of course) a double click on the DLL should start the NUnit GUI runner. It has a 'RUN' button. Click once, and the first test run should start showing 'green'. ( Test cycles are named 'Make it green'.)
After the first run the tests starts automaticly after each compile. One or two or thousands some time. We see a few moments after some code changes does it introduce a bug (red)or not (green).
It makes you very confident to your software, when it remains green. And if it changes to red, you found the bug before your customer does. This is better than running debug sessions hours later.
Now the complete example. Some comments on it: I don't claim, it's well designed. These are one of the first tests I wrote 2012. I just copied and pasted.
Purist say: Only one test method for one test case. The reason for this is: If we have more then one tests in a test method and one of these tests fail, the remaining will be skipped. It's in most cases easier to analyse the issue if we know, what test fail and what test passes.
Here I follow this rule for the exceptions test. The other tests are all in one test method. It reads much clearer.
In general I follow the rule, because many test have to be prepared/initialised by setting parameters, environment and so on.
Ok, enough silly words, here is the full example code:
// Functions.prg, contains the business code
PARTIAL STATIC CLASS Functions
STATIC PRIVATE ASC0 := 48 AS INT // Example for a private member
// The example is the well known STUFF function. The parameters are the same as in VO, for example StartPosition is one-based. But instead reading some kind of documentaion
// the test should show what happens. (Sorry for mixing german/english in code)
STATIC METHOD Stuff( SELF InputStr AS STRING, StartPosition AS INT, AnzahlZumLöschen AS INT, ErsatzZeichen AS STRING ) AS STRING // Replacement for runtime Stuff-Function
// Check the input parameter
IF InputStr == NULL
THROW System.ArgumentNullException{ "InputStr is NULL." }
ENDIF
IF StartPosition < 1
THROW System.ArgumentOutOfRangeException{ "Startposition is "+StartPosition:ToString()+". Must be 1 or greater." }
ENDIF
LOCAL RetValue AS System.Text.StringBuilder
RetValue := System.Text.StringBuilder{}
IF StartPosition-1 < 0 // Code, that should never be executed, because it's excluded by parameter checks.
StartPosition := 1 // This should be discovered by a Code Coverage Tool (NCover). This is dead code, but not obvious.
ENDIF
RetValue:Append( InputStr:Substring( 0, StartPosition-1 ) ) // Without the parameter checks we would get generic, means mysterious, error message.
IF ErsatzZeichen != NULL
RetValue:Append( ErsatzZeichen )
ENDIF
IF StartPosition-1+AnzahlZumLöschen < InputStr:Length
RetValue:Append( InputStr:Substring( StartPosition-1+AnzahlZumLöschen ) )
ENDIF
RETURN RetValue:ToString()
END CLASS
// FunctionsTests.prg, contains the tests
#ifdef NUNIT
#using NUnit.Framework
PARTIAL STATIC CLASS Functions
[TestFixture] ;
SEALED CLASS FunctionsTests INHERIT AssertionHelper
[Test] ;
METHOD Private_Asc0( ) AS VOID // Only to show how to access private member of the outer class.
Expect( Functions.Asc0, Is.EqualTo( 48 ) )
[Test] ;
METHOD Stuff_ArgumentNullException_InputString_0( ) AS VOID
Assert.Throws<ArgumentNullException>( TestDelegate{ SELF , @_Stuff_ArgumentNullException_InputString_0() } )
PRIVATE METHOD _Stuff_ArgumentNullException_InputString_0( ) AS VOID
Functions.Stuff(NULL, 2, 0, "xyz")
[Test] ;
METHOD Stuff_ArgumentOutOfRangeException_Startosition_0( ) AS VOID
Assert.Throws<ArgumentOutOfRangeException>( TestDelegate{ SELF , @_Stuff_ArgumentOutOfRangeException_Startosition_0() } )
PRIVATE METHOD _Stuff_ArgumentOutOfRangeException_Startosition_0( ) AS VOID
Functions.Stuff("dummy", 0, 0, NULL ) // Possible through SELF in methoden declaration (extension method).
[Test];
METHOD Stuff( ) AS VOID // All simple tests in one method
Expect( Functions.Stuff("ABCDEF", 2, 0, "xyz") , Is.EqualTo( "AxyzBCDEF" ), "Fall 1" )
Expect( Functions.Stuff("ABCDEF", 2, 3, "xyz") , Is.EqualTo( "AxyzEF" ) , "Fall 2" )
Expect( Functions.Stuff("ABCDEF", 2, 2, NULL) , Is.EqualTo( "ADEF" ) , "Fall 3" )
Expect( Functions.Stuff("ABCDEF", 2, 1, "xyz") , Is.EqualTo( "AxyzCDEF" ) , "Fall 4" )
Expect( Functions.Stuff("ABCDEF", 2, 4, "xyz") , Is.EqualTo( "AxyzF" ) , "Fall 5" )
Expect( Functions.Stuff("ABCDEF", 2, 5, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 6" )
Expect( Functions.Stuff("ABCDEF", 2, 6, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 7" )
Expect( Functions.Stuff("ABCDEF", 2, 7, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 8" )
Expect( Functions.Stuff("ABCDEF", 2, 8, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 9" )
Expect( Functions.Stuff("ABCDEF", 2, 10, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 10" )
Expect( Functions.Stuff("ABCDEF", 1, 0, "xyz") , Is.EqualTo( "xyzABCDEF" ), "Fall 11" )
Expect( Functions.Stuff("ABCDEF", 1, 3, "xyz") , Is.EqualTo( "xyzDEF" ) , "Fall 12" )
Expect( Functions.Stuff("ABCDEF", 1, 2, NULL) , Is.EqualTo( "CDEF" ) , "Fall 13" )
Expect( Functions.Stuff("ABCDEF", 1, 1, "xyz") , Is.EqualTo( "xyzBCDEF" ) , "Fall 14" )
Expect( Functions.Stuff("ABCDEF", 1, 4, "xyz") , Is.EqualTo( "xyzEF" ) , "Fall 15" )
Expect( Functions.Stuff("ABCDEF", 1, 5, "xyz"), Is.EqualTo( "xyzF" ) , "Fall 16" )
Expect( Functions.Stuff("ABCDEF", 1, 6, "xyz"), Is.EqualTo( "xyz" ) , "Fall 17" )
Expect( Functions.Stuff("ABCDEF", 1, 7, "xyz"), Is.EqualTo( "xyz" ) , "Fall 18" )
Expect( Functions.Stuff("ABCDEF", 1, 8, "xyz"), Is.EqualTo( "xyz" ) , "Fall 19" )
Expect( Functions.Stuff("ABCDEF", 1, 10, "xyz"), Is.EqualTo( "xyz" ) , "Fall 20" )
Expect( "ABCDEF":Stuff( 1, 10, "xyz") , Is.EqualTo( "xyz" ) , "Fall 21" )
END CLASS
for all of you coming directly from VO to X#, I will introduce you to something that was not possible with VO: Unit Testing.
For more detailed information I strongly recommend these two books:
Working Effectivly with Legacy Code by Micheal C. Feathers
and
The Art of Unit Testing, by Roy Osherove
Both are available in german, so I think, they are available in many other languages too.
More books to read may be found here:
http://www.codeproject.com/Reference/61 ... ence-Books
Now, what is Unit Testing? In short words:
- It's the first defense line against bugs!
and
- it prevents in many cases the use of a debugger. I feel, and since I do unit testing more than ever, that debugging is mostly a waste of time.
The biggest con: it cannot be repeated automaticly. We have to do it manual. It's like shooting in the dark.
Unit testing is more like engineering, is quality control, beginning from the first line of code to the last one. And at the ends it saves the value of your code base.
Software without unit tests is effectivly unsaleable! Who wants to buy a pig in a poke? And what prevents against unwanted side effects of code changes.
Now, as always, the first step is the hardest. But I promise you, as soon as you have found your first hidden bug, you never will miss the tests. For me it took only one hour.
I do unit testing with XIDE and NUNIT. http://www.nunit.org/
For now I recommend to download and install V 2.6. The simple reason: Until today they do not have a GUI runner for V 3, which I like to use. But be careful, don't use features that are flagged deprecated.
Then we do some settings in XIDE.
First we create a new project configuration. I name it NUNIT. Project->Properties->Configuration->Add
Now we create a new application.
I recommend as a general tip to set one common output directory for all applications of a project. In fact I use the same directory for all my projects.
Application->Properties->General->Output folder:
For example I name it D:InstallSource/Assemblies
We copy the NUnit framework assembly nunit.framework.dll into this directory.
At Application->Properties->General->Compiler->Build configurations->NUnit we insert
/define:NUNIT
At Application->Properties->References->Browse we click 'Browse disk for dll files' and search and select nunit.framework.dll .
The program and the corresponding tests are put into two separate PRG's. For example
Functions.prg and FunctionsTests.prg
This way they are always grouped together.
First I show the general frame, where the business code and the test code is in. My example is a function, where the class is a static class, that cannot be instantiated.
But the schema is always the same.
// Functions.prg, contains the working code, that has to be tested.
PARTIAL STATIC CLASS Funktionen // important: PARTIAL!
...
END CLASS
// FunktionenTests.prg, contains the tests
#ifdef NUNIT // Compile only, if define NUNIT exists, see project configuration and application setting.
#using NUnit.Framework
PARTIAL STATIC CLASS Funktionen // Seems to be senseless. But through using the 'Nested Class' construct allowas the tests access to private members tested class!
[TestFixture] ;
SEALED CLASS FunktionsTests INHERIT AssertionHelper // AssertionHelper is a NUnit class.
...
END CLASS
END CLASS
#endif
I think, this is easier to recognize then embedded in ful example. This frame can be saved as a code snippet.
After the first compile (complete example of course) a double click on the DLL should start the NUnit GUI runner. It has a 'RUN' button. Click once, and the first test run should start showing 'green'. ( Test cycles are named 'Make it green'.)
After the first run the tests starts automaticly after each compile. One or two or thousands some time. We see a few moments after some code changes does it introduce a bug (red)or not (green).
It makes you very confident to your software, when it remains green. And if it changes to red, you found the bug before your customer does. This is better than running debug sessions hours later.
Now the complete example. Some comments on it: I don't claim, it's well designed. These are one of the first tests I wrote 2012. I just copied and pasted.
Purist say: Only one test method for one test case. The reason for this is: If we have more then one tests in a test method and one of these tests fail, the remaining will be skipped. It's in most cases easier to analyse the issue if we know, what test fail and what test passes.
Here I follow this rule for the exceptions test. The other tests are all in one test method. It reads much clearer.
In general I follow the rule, because many test have to be prepared/initialised by setting parameters, environment and so on.
Ok, enough silly words, here is the full example code:
// Functions.prg, contains the business code
PARTIAL STATIC CLASS Functions
STATIC PRIVATE ASC0 := 48 AS INT // Example for a private member
// The example is the well known STUFF function. The parameters are the same as in VO, for example StartPosition is one-based. But instead reading some kind of documentaion
// the test should show what happens. (Sorry for mixing german/english in code)
STATIC METHOD Stuff( SELF InputStr AS STRING, StartPosition AS INT, AnzahlZumLöschen AS INT, ErsatzZeichen AS STRING ) AS STRING // Replacement for runtime Stuff-Function
// Check the input parameter
IF InputStr == NULL
THROW System.ArgumentNullException{ "InputStr is NULL." }
ENDIF
IF StartPosition < 1
THROW System.ArgumentOutOfRangeException{ "Startposition is "+StartPosition:ToString()+". Must be 1 or greater." }
ENDIF
LOCAL RetValue AS System.Text.StringBuilder
RetValue := System.Text.StringBuilder{}
IF StartPosition-1 < 0 // Code, that should never be executed, because it's excluded by parameter checks.
StartPosition := 1 // This should be discovered by a Code Coverage Tool (NCover). This is dead code, but not obvious.
ENDIF
RetValue:Append( InputStr:Substring( 0, StartPosition-1 ) ) // Without the parameter checks we would get generic, means mysterious, error message.
IF ErsatzZeichen != NULL
RetValue:Append( ErsatzZeichen )
ENDIF
IF StartPosition-1+AnzahlZumLöschen < InputStr:Length
RetValue:Append( InputStr:Substring( StartPosition-1+AnzahlZumLöschen ) )
ENDIF
RETURN RetValue:ToString()
END CLASS
// FunctionsTests.prg, contains the tests
#ifdef NUNIT
#using NUnit.Framework
PARTIAL STATIC CLASS Functions
[TestFixture] ;
SEALED CLASS FunctionsTests INHERIT AssertionHelper
[Test] ;
METHOD Private_Asc0( ) AS VOID // Only to show how to access private member of the outer class.
Expect( Functions.Asc0, Is.EqualTo( 48 ) )
[Test] ;
METHOD Stuff_ArgumentNullException_InputString_0( ) AS VOID
Assert.Throws<ArgumentNullException>( TestDelegate{ SELF , @_Stuff_ArgumentNullException_InputString_0() } )
PRIVATE METHOD _Stuff_ArgumentNullException_InputString_0( ) AS VOID
Functions.Stuff(NULL, 2, 0, "xyz")
[Test] ;
METHOD Stuff_ArgumentOutOfRangeException_Startosition_0( ) AS VOID
Assert.Throws<ArgumentOutOfRangeException>( TestDelegate{ SELF , @_Stuff_ArgumentOutOfRangeException_Startosition_0() } )
PRIVATE METHOD _Stuff_ArgumentOutOfRangeException_Startosition_0( ) AS VOID
Functions.Stuff("dummy", 0, 0, NULL ) // Possible through SELF in methoden declaration (extension method).
[Test];
METHOD Stuff( ) AS VOID // All simple tests in one method
Expect( Functions.Stuff("ABCDEF", 2, 0, "xyz") , Is.EqualTo( "AxyzBCDEF" ), "Fall 1" )
Expect( Functions.Stuff("ABCDEF", 2, 3, "xyz") , Is.EqualTo( "AxyzEF" ) , "Fall 2" )
Expect( Functions.Stuff("ABCDEF", 2, 2, NULL) , Is.EqualTo( "ADEF" ) , "Fall 3" )
Expect( Functions.Stuff("ABCDEF", 2, 1, "xyz") , Is.EqualTo( "AxyzCDEF" ) , "Fall 4" )
Expect( Functions.Stuff("ABCDEF", 2, 4, "xyz") , Is.EqualTo( "AxyzF" ) , "Fall 5" )
Expect( Functions.Stuff("ABCDEF", 2, 5, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 6" )
Expect( Functions.Stuff("ABCDEF", 2, 6, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 7" )
Expect( Functions.Stuff("ABCDEF", 2, 7, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 8" )
Expect( Functions.Stuff("ABCDEF", 2, 8, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 9" )
Expect( Functions.Stuff("ABCDEF", 2, 10, "xyz"), Is.EqualTo( "Axyz" ) , "Fall 10" )
Expect( Functions.Stuff("ABCDEF", 1, 0, "xyz") , Is.EqualTo( "xyzABCDEF" ), "Fall 11" )
Expect( Functions.Stuff("ABCDEF", 1, 3, "xyz") , Is.EqualTo( "xyzDEF" ) , "Fall 12" )
Expect( Functions.Stuff("ABCDEF", 1, 2, NULL) , Is.EqualTo( "CDEF" ) , "Fall 13" )
Expect( Functions.Stuff("ABCDEF", 1, 1, "xyz") , Is.EqualTo( "xyzBCDEF" ) , "Fall 14" )
Expect( Functions.Stuff("ABCDEF", 1, 4, "xyz") , Is.EqualTo( "xyzEF" ) , "Fall 15" )
Expect( Functions.Stuff("ABCDEF", 1, 5, "xyz"), Is.EqualTo( "xyzF" ) , "Fall 16" )
Expect( Functions.Stuff("ABCDEF", 1, 6, "xyz"), Is.EqualTo( "xyz" ) , "Fall 17" )
Expect( Functions.Stuff("ABCDEF", 1, 7, "xyz"), Is.EqualTo( "xyz" ) , "Fall 18" )
Expect( Functions.Stuff("ABCDEF", 1, 8, "xyz"), Is.EqualTo( "xyz" ) , "Fall 19" )
Expect( Functions.Stuff("ABCDEF", 1, 10, "xyz"), Is.EqualTo( "xyz" ) , "Fall 20" )
Expect( "ABCDEF":Stuff( 1, 10, "xyz") , Is.EqualTo( "xyz" ) , "Fall 21" )
END CLASS