Project SummaryA unit testing solution for Visual Studio 2008 Express editions, allowing easy writing, running and debugging of unit tests within Express.
ObjectivesA simple way to write, run and debug unit tests within Visual Studio 2008 Express editions.
Avoid the need for attributes in order to reduce the code size.
Reduce the scope when declaring an expected exception to just the line of code expected to throw the exception.
I've been using this stuff for ages, but after last year's survey of Express users about unit testing, I thought it might be worth posting up once I had some time to clean it up.
If this project generates any interest or requests to extend the funtionality, then I'll try and mature it to an official 1.0 release. Alternatively of course you might just want to take the code and extend it to work the way you want it to.
If voluntary collaborators appear I will move the source code the Sub Version tab, but for now just get the source code from the Release.
OverviewThis is a simple lightweight Unit Testing library. It can be launched from within .Net so can be used to debug Unit Tests in VS 2008 Express. Note that if your using anything else but Express then there are better Unit Testing options available to you and this one will serve little more than a curiosity.
So this is not meant to be a framework to replace any other Unit Test framework, just to allow me to debug unit tests in Express. Given last year's survey of Express users, it's possible that VS 2010 Express may actually come with the Unit Testing incorporated, or better still, be extendable. In the mean time however. Until then this is how I do unit testing under Express.
I originally wrote TestLib with just the Inspect & InspectException class to help testing of private members with NUnit. This was back in .Net 1.0 days.
Later I wanted to be able to run my unit tests directly under the VS2005 Express IDE, so I added the remaining classes to be able to perform simple unit testing on the fly. My objectives were minimum coding to write this library and minimum coding to write the unit tests. This is based on my opinion that two of the most important aspects of unit tests is that they should be cheap to write and cheap to run.
From time to time I've updated and refined this library. There is a bit of LINQ in there now so it's now VS2008 Express only (though it is easy enough to replace if you want to go back to VS2005).
Early on I started with a lot of attributes but in the end I got rid of most of them. I've left [ExpectException] in there to show how the attributes worked but I've even gone off it in favour of the ExpectException() method. Consider the two following versions of the same test method:
[ExpectException( typeof( ArgumentOutOfRangeException)]
public void TransferWithNegativeAmountTest()
{
Account fromAccount = AccountManager.GetAccountById( 1);
Account toAccount = AccountManager.GetAccountById( 2);
fromAccount.Transfer( toAccount, -1);
}
public void TransferWithNegativeAmountTest()
{
Account fromAccount = AccountManager.GetAccountById( 1);
Account toAccount = AccountManager.GetAccountById( 2);
Test.ExpectException( typeof( ArgumentOutOfRangeException));
fromAccount.Transfer( toAccount, -1);
}
The difference is the first version has the attribute at start, and the second version has the Test.ExpectException() call below. Note that the Test class is my equivalent of the Assert class of most other unit test frameworks.
First up, note that the tests involve exactly the same number of lines of code assuming you count the attribute as a line of code.
But more importantly, the second version will fail if either of the first two lines of code raise an ArumentOutOfRangeException, where as the first version would not. Basically the method allows us to explicitly state where in the test an exception is expected, which should essentially be the last line of code in the test.
I'm not anti-attributes by the way. They're great things, but as for declaring test classes and or assemblies, I've just gone for a simple naming syntax:
- all test assembly names must end with "Tests"
- all test class names must end with "Tests", are public, non-static and have a public constructor that takes not arguments
- all test methods names must end with "Test" and have the signature "public void name()"
This way I avoid one extra line for an attribute to mark every test class, and more specifically every test method, which provides a saving as the number of tests increase.
Use the static "Test" class to perform checks in your test. It includes the following methods:
| .Fail(...) | - Makes the test fail |
| .True(...) | - Makes the test fail if the specified expression yields false |
| .False(...) | - Makes the test fail if the specified expression yields true |
| .NotNull(...) | - Makes the test fail if the specified expression yield null |
| .Null(...) | - Makes the test fail if the specified expression yields other than null |
| .SameObject(...) | - Makes the test fail if the specified expressions do not yield references to the same instance |
| .Equal(...) | - Makes the test fail if the specified expressions do not yield the same value |
| .NotEqual(...) | - Makes the test fail if the specified expressions yield the same value |
| .Larger(...) | - Makes the test fail if the second expession yields a value larger or equal to the first expression |
| .LargerOrEqual(...) | - Makes the test fail if the second expession yields a value larger than the first expression |
| .Smaller(...) | - Makes the test fail if the second expession yields a value smaller or equal to the first expression |
| .SmallerOrEqual(...) | - Makes the test fail if the second expession yields a value smaller than the first expression |
| .ExpectException(...) | - Makes the test fail if the remaining test does not raise an exception of the specified type |
| .Field(...) | - Makes the test fail if the specified field (data member) does not have the expected value |
| .Property(...) | - Makes the test fail if the specified property does not yield the expected value |
| .Method(...) | - Makes the test fail if the specified method does not return the expected value |
Note that .Field(), .Property() and .Method() can be used to test non public members. You can also use the Inspect class to access non-public members. In fact these "Test" methods just call the Inspect class anyway. Public members work with these methods too but you might as well as access them directly and get the intelli-sense support and a little less code.
The Inspect static class can be used to both read and write to these private members. Public members work too but same as above. The Insepect methods are:
| .CallMethod(...) | - Calls a method with a variable list of arguments and gets the result |
| .CallMethodWithArray(...) | - Calls a method with a list of arguemnts in an array and gets the result |
| .GetProperty(...) | - Gets the value of a property |
| .SetProperty(...) | - Sets the value of a property |
| .GetField(...) | - Gets the value of a data member |
| .SetField(...) | - Sets the value of a data member |
| .CallConstructor(...) | - Calls a constructor with a list of arguments and returns the instance |
Using TestLib1) Write a test assembly:
a) Add a new class library project to your solution with a name that ends with "Tests" (you can have more than one).
b) Add a project reference for the project you are testing.
c) Add a reference to TestLib.dll to that project.
d) Add a pulic non-static class to the the project with a name that ends with "Test".
e) Ensure you have a default (no arguments) constructor, either the implicit or an explicit one.
f) For each test, add a method with a name that ends with "Test" that is public, void and takes no arguments
2) Write the unit test executer.
a) Add a new console project to the solution.
b) Add a reference to TestLib.dll to this project.
c) Add a project reference for each of the test assemblies.
d) Make the Main() method do something like:
static void Main(string[] args)
{
Tester tester = new Tester();
tester.Execute();
string report = tester.GetBreifReport();
Console.WriteLine(report);
Console.WriteLine();
Console.WriteLine("(Press [Enter] to close.)");
Console.ReadLine();
}
3) Run the Unit Tests.
a) Usually best to save and rebuild all but no essential.
b) Set the console project as the start project if not elready.
c) Run the console project and view the results.
Note that Tester also has a GetFullReport() method that lists all tests executed, not just the ones that failed.
Tester also has a public Results property that is a collection of TestResult objects once the tests have been executed.
What's not there (though most could be easily added)i) TestLib does not create a separate application domain to run the tests. They run as the console exe.
ii) TestLib does not have any metrics other than a total count of tests that passed and failed.
iii) TestLib has no GUI front end, or links that directly jump to the code. A GUI front end is easy enough to do, but I'm not sure Visual Studio Express will let you provide links directly to the code. And of course if your using an edition of Visual Studio that can be extended, then you will probably want to use one of the more popular unit testing framewords. (The MS one if fine. NUnit is my other favourite).
iv) No built in means for mocking, though it should work with any reasonable mock library.
v) The test class is only substantiated once, and then every test method is callled. This allows you to support state between tests if you want. It is easy enough to change however if you want it to recreate the test class for every method.
vi) There is no automatic build up or tear down method. You can either declare these explicitly or again, it would be easy enought to modify the code to run methods of specific names if present.
I've also considered writing versions of this to run Unit Tests written in MS and or NUnit. It's doable, but because these framewords have differences in how their executed, it is not as straight fowards. For example, NUnit creates a separate application domain to run it's tests under and each test assembly can have it's own config file.