Contents
Introduction
The Setup
How it works
Notes & Observations
Conclusion
Revision History
If, like me, you have quite a large code base that many of your projects use, there is always the risk that if you change something in your base classes for one dependent project, it may break another dependent project. This was a problem for us at work, where all our projects use a library for data access, which can get changed quite often, and as a result could break many projects. To combat this I was assigned the task of writing a test function in each class, which tested all the public methods of the class. I then wrote an application that could automatically run all the tests. So, this is an article on how to use Reflection, as I couldn?t find anything to really help me achieve this.
The project is split into three parts: an interface, the item(s) to be tested, and the test application.
The interface is a single class library, with two parts, the interface itself, and a public enum, which I use to give the test a result type. Entire Testbench.vb code listing:
Public Interface TestBench
Function Test(ByVal log As List(Of String)) As TestResult
End Interface
Public Enum TestResult
Fail = 0
Fail_Data_Error
Fail_Exception
Fail_Timeout
Pass = 20
Pass_No_Test_Needed
End Enum
The Interface has only one function in it, a Test function with a list of strings to provide feedback with, and a result type to send back at the end of the test. The other item in the file is a padded enum. The enum is padded to allow more pass and fail states to be added at a later date. Anything less than 20 is a fail, and anything greater than or equal to 20 is a pass.
The Test items are also simple to write. All that is required is a reference in the project to TestBench.dll, and for your classes to implement the interface. In the example I provide 3 classes to show what you can do. The code listing for one of the items to be tested:
Public Class PassingItem
Implements TestBench.TestBench
Public Function Test(ByVal log As System.Collections.Generic.List(Of String)) _
As TestBench.TestResult Implements TestBench.TestBench.Test
log.Add("This is a test, one that passes")
Return TestBench.TestResult.Pass
End Function
End Class
As you can see, all I have done is add the interface and hit enter, which automatically puts the function into your class. I then added a single entry to the log, and return the desired result type. If you have the function in the class, but without the implements statement, the test app will not pick it up, as it looks for a class with the interface in it.
The test application for the purposes of this article is simple; it contains one listbox, and one button. The listbox shows us the log of the tests, and the button starts the test. At the core of the test function is the following code:
Dim assembly As Reflection.Assembly = Reflection.Assembly.LoadFrom(path)
lst.Items.Add(String.Format("Assembly {0} Loaded", assembly.FullName))
For Each t As Type In assembly.GetTypes
If t.IsClass AndAlso t.GetInterface("TestBench") IsNot Nothing Then
lst.Items.Add(String.Format("---Starting test for {0}", t.Name))
_log.Clear()
Dim r As TestBench.TestResult = DirectCast(Activator.CreateInstance(t), _
TestBench.TestBench).Test(_log)
lst.Items.AddRange(_log.ToArray)
lst.Items.Add(String.Format("---Finished Testing {0}, with result: {1}", _
t.Name, r.ToString))
End If
Next
Let's go through it, and see what does what.
Dim assembly As Reflection.Assembly = Reflection.Assembly.LoadFrom(path)
This line creates a new Assembly, which is then set to the contents of the selected DLL. There is a slight problem with this however; it keeps the DLL locked until your application quits. While in some situations this may be no problem, this can be annoying if you are constantly recompiling the target dll, as VS will detect that the DLL is locked, and won?t write the output file. The way in which I solved this was suggested to me by John H. of the
VB Dot Net Forums. His suggestion was to use
ReadAllBytes() function in the
System.IO.File namespace. This allows you to load the DLL in without locking it. The replacement of the line above is:
Dim assembly As Reflection.Assembly = Nothing
assembly = Reflection.Assembly.Load(System.IO.File.ReadAllBytes(path))
Next is the loop:
For Each t As Type In assembly.GetTypes
If t.IsClass AndAlso t.GetInterface("TestBench") IsNot Nothing Then
End If
Next
This loops through all of the Types in the assembly. There are a lot of these included which we have no interest in, such as
TestITem.My.MyProject+MyWebServices and
TestITem.My.Resources.Resources, so we need to check that the Type loaded is a Class and that it implements our interface. To do this we use
t.GetInterface() and check if it is not nothing, if it is nothing, the interface is not present, and if it is not null, we have a testable class. The next part is the most important part of the testing process, creating an instance of the class, and running the
Test() function. First off, we create a variable to store the test result in.
Dim r As TestBench.TestResult = Nothing
We then use the
Activator.CreateInstance() function to create an instance of our class, and use
DirectCast to cast it to the interface, which allows us to execute anything contained in the interface, in this case, the
Test() function.
r = DirectCast(Activator.CreateInstance(t), TestBench.TestBench).Test(_log)
There is no error handling in this code, I know. This is to make the code very easy to read and understand, and what levels of error protection you wish to put in are up to you. Good places to start are Timeout and Null Reference exceptions in the actual test itself, and have the exception messages go to the log. You
must have a blank constructor for your class to be tested. By default if you have not specified a
Public Sub Main() of any sort in the class, it will work, but if you have a constructor with parameters , you must also have a blank one, even if it is only there for use with your Test Bench.
I hope this article was of some use to you, and that you can extend the basic methods mentioned here to create some automated testing for your code. Code based round this method has been very useful at work, and helps spot errors in code and new Database Constraints of which the code may be in violation.
20th January 2008 - Article Released