Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to use Reflection to test your code

0.00/5 (No votes)
20 Jan 2008 2  
Demonstrates how to use Reflection to run test functions inside your compiled assemblies.

Contents

Introduction

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 Setup

The project is split into three parts: an interface, the item(s) to be tested, and the test application.

The Interface

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. Here is the 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 Item

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 three classes to show what you can do. Here is 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 add 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 Testing Application

The test application for the purposes of this article is simple; it contains a listbox and a 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.

How it Works

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, it 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 the 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)

Notes and Observations

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.

Conclusion

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 around 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.

Revision History

  • 20 January 2008 - Article released.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here