When I was doing the rounds at the recent DotNetNuke OpenForce conference, I was surprised at the number of people who wanted to talk to me about the DNN Unit Testing framework I put together a few years back. I use this on an almost daily basis, but it’s become part of the plumbing for all projects so I don’t really think about it as much as I used to. I was recently contacted and asked to update it all for DNN 5, and I thought I would spend some time bringing it up to latest standards and generally re-visit the topic again. It would seem there is a lot of interest in doing Test Driven Development with DNN, so this post will revisit all of that.
Should you be doing Test Driven Development with DotNetNuke?
Test Driven Development (TDD) is a worthy goal of any software project that aspires to good quality and the flexibility to change over time. If this is true of your DotNetNuke module, then the answer is yes. All non-trivial iFinity modules have Unit Test projects that are run before releases and after changes to check for breaking changes. This has caught a number of errors over the years, and prevented them from showing up in an actually released software (sadly the unit test coverage is not 100%, hence some bugs do get out, rotten little things!)
What will you need?
All of the information presented in this post covers DotNetNuke 5.1.2 and later, uses Visual Studio 2008 Team Edition and was done on a Windows 7 machine. None of this is really necessary : you don’t have to use the in-built unit testing of Visual Studio (nUnit will do) and you can really use any version of Visual Studio you like, as long you’ve hooked up a unit testing framework. I will suggest, however, that you have loads of fast RAM and a blazing fast processor, or you will soon bore of the constant compiling, running and copying that is part of unit testing. I’m not going to pretend that the process of setting up unit testing with DNN is straightforward, so any amount of frustration you can remove by throwing hardware at the problem will be justified (see, print that bit out and go ask your boss for a new computer).
Note, if you aren’t using the Visual Studio test tools, then you’ll have to do some refactoring to the code to get it to work. If you do manage to, say, get it working with nUnit, let me know via the contents and I can make your work available (or write a blog post somewhere and I’ll reference it).
Contents of the Download
In my previous posts on this topic, I provided some DLLs and general instructions on setting up some procedures. While this was useful, I decided to go a bit further and actually provide a complete example that you can download and setup, and (hopefully) press the magic ‘go’ button and see some tests running. This is probably a very lofty goal given the territory, but everything should be there.
Solution Contents
When you download the solution, you’ll see the following projects:
- Solution items : the testSetupScript.cmd file, which prepares the test output for running a mock DNN environment, the ‘testrunconfig’ file, which references items to be deployed with the test, and the .vsmdi file, which contains a list of unit tests in the solution
- iFinity.Basic.UnitTest.Example project. This is the unit test project. It contains a single test class file, as well as the various config files required to make it work with DNN
- iFinity.DNN.Utilities : This is the Unit Testing framework I built to work with DNN. It contains the code to create the Mock DNN environment in which to run unit tests
- iFinity.ExampleModule, iFInity.ExampleModule.Data : these two projects are pretend module DLLs. This article is all about unit testing Private Assembly modules, and this setup is the standard way to construct a DNN PA module. Note there is no UI code in these projects, just a class, a module controller and a data provider. The idea is just to test the parts of the code that use the DNN framework (namely, reading/writing to the database)
How to use the download
1) Download the code, extract the zip file, open it up in your Visual Studio (note, you’ll need a version which contains the Unit Testing framework, this won’t work in the ‘free’ editions).
2) Update the connection string in the app.config file to point at a DNN database that you have lying about somewhere. If you don’t have a local test DNN database, then create a new instance of DNN just for unit testing purposes. Actually, that’s not a bad idea anyway. You can either use a .mdf file in the ‘user instance’ mode, or connect to a sql server database somewhere. It depends on your objectives with testing and what you’ve got available.
3) Find the 01.00.00.SqlDataProvider file in the ‘iFinity.ExampleModule.Data’ project, under the ‘SqlDataProvider’ directory. Then run this through the ‘host->sql’ page of the DNN install that belongs to your DNN database you’re going to use for testing. If you can’t do that, then run the Sql in a normal sql query editor, but remember to find/replace the ‘{objectQualifier}’ and ‘{databaseOwner}’ tokens first.
4) Start the Unit tests with alt-shift-x, or find and click the ‘run tests’ button in Visual Studio (hint, it’s not the green “play” button that starts debugging)
5) Check the test results to see if you got it to work OK.
If you have any custom modules in your DNN install that need configuration changes, you might need to change the app.config file in your test project to match the web.config file of the associated DNN install. It’s very important to realise that you are running a mock DNN application, and that mock application is using the app.config file of the test project, not the web.config file of the DNN project.
Test Setup
When you hit the magic ‘test it all for me’ button, there’s several things that go on. In the background, the Unit Testing framework is compiling the versions of your components, and copying them to a special directory, created underneath the ‘TestResults’ directory. By default this is in the solution folder. However, this is just a flat directory with a pile of DLL’s in it : not unlike the \bin directory of a standard DNN install. Our aim here is to make the DNN code run in a test context, and to do that, some parts of the DNN framework have to be re-created.
This is achieved by the ‘TestSetupScript.cmd’ file. This file simply creates a couple of bare-bones directories in the OUT directory of the test, to make it look a bit more like a DNN install to the DotNetNuke code that will be running in the test. Note that the portal is hard-coded in this script file, to match the entries in the ‘dnnUnitTest’ section. This file needs to be hooked up to the ‘TestRunConfig’ file in the solution containing the Test project. This tells the Test framework to run the file before testing starts.
If you don’t set up this file, the code in the DNNUnitTest class coughs up a helpful error message, which will look something like this:
Unable to create instance of class iFinity.DNN.Modules.ExampleModule.Test.ExampleRecordInfoDBTest. Error: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: Supplied HostMapPath [..\iFinity.UnitTesting.Examples\TestResults\Bruce_WOMBAT 2009-12-02 14_35_39\Out\Website\portals\0\] is not valid. Specify a valid path on the target machine (Did you forget to run the testSetupScript.cmd file to create the portals directory??)
This message is supposed to be a slap to the forehead “Hey, wake up and include the TestSetupScript.cmd file!”
There are other files that DotNetNuke needs to run, as well. These include the DotNetNuke.config file, and the SiteUrls.config file. Both of these files will be searched for by the DNN framework as it is loaded. So, these two files are also specified as required items in the .testrunconfig file, in the ‘Deployment’ section. This ensures that they will be copied to the OUTPUT directory before testing starts.
A note on project referencing and Test setup
You’ll see that the UnitTest project contains a reference to the ‘Data’ project : namely the iFinity.ExampleModule.Data project (and assembly). Strictly speaking the Unit Test project shouldn’t have a reference to this project, the database calls should go through the ModuleController class. However, referencing the project gives two shortcuts. The first is that when a module is referenced by the test project, the compiled assembly is automatically copied to the OUT directory for testing. The second is that it allows us to run cleanup database code directly against the database by utilising the connectionString reference that the SqlDataProvider object contains. If this all makes you feel uncomfortable in a ‘impure architecture’ way, feel free to concoct your own solution. But I’m here to tell you that getting the assembly manually copied is inelegant as well, and opening the app.config file to look for the connection string is ‘wordy’ as well.
How it Works
This framework works by creating a Mock (or fake) ASP.NET runtime context to run your unit tests within. Tests normally run in the Visual Studio Test context – and if you do this, you can’t access server objects like HttpContext : which are vital for a web-based application like DotNetNuke.
This fits together because the TestClass inherits from the DNNUnitTest class contained within the ‘iFinity.DNN.Utilities’ project. When the TestClass is instantiated to run the tests contained within, the constructor code in the DNNUnitTest class kicks off the type of initialisation normally run within the DNN startup code. It also constructs a mock HttpContext object, and stores some of the key DNN global objects (like PortalSettings and HostSettings) in the HttpContext items. It also loads some of the Providers used by DNN for key tasks such as database access, data caching and others.
Thus when your code runs and makes calls to items within the DotNetNuke namespace, the actual objects are there to take the calls and return the results. This allows you to create unit tests to run pieces of code within your module that would otherwise fail because there was no DotNetNuke object there to receive and process them.
Issues to Understand
Windows Vista/7/2008 Security
If you’re like me and have upgraded to the latest Windows offerings, you’ll soon run aground on the fact that Unit Testing is considered a highly risky business by the born-again security converts on the Windows team. You may get an error like this one:
Test Run deployment issue: The location of the file or directory 'D:\DotNetNuke\Custom\Common\iFinity.UnitTesting.Examples\dnn510\DotNetNuke.dll' is not trusted.
This is because the DLL’s weren’t compiled on the machine you’re testing on, so you’re guilty of running code that someone else wrote.
The generally accepted solution is to ‘unblock’ the file(s) that cause the problem. You should do this in the location they are referenced from rather than the location noted in the error message. This is because when you debug and test, the new copies will be taken from the reference location and copied into the destination location. To unblock the file, right click on the offending file, select ‘Properties’ and go to the ‘General’ tab. At the bottom you’ll see a message looking something like this:
Click ‘unblock’ at the bottom, and you should be OK. You might have to restart Visual Studio to fix this problem, so I’d suggest unblocking all of the referenced files at once, then doing a restart of VS – just to be sure. A big thanks to whomever at Microsoft decided this was the way to go.
If that doesn’t solve the problem for you, take a look at this StackOverflow topic, and see if you can glean some help. You might also try recompiling the DNN solution on your local computer, and using the output from that. This also has the benefit of giving up some up-to-date .pdb files to help with any debugging.
Changes for DNN 5 : the ‘Cannot register or retrieve components until ComponentFactory.Container is set’ error
As previously mentioned, this blog post came about as a result of people asking me about a DNN 5 compatible version of this code. The big change comes about through the use of the ‘ComponentFactory’ component used to handle the loading of providers and components in DNN 5. This completely changes the way that 4.x DNN used to load components on startup.
I was helped immensely by Charles Nurse, who has an excellent series on creating testable modules on his blog : http://www.charlesnurse.com/post/Creating-Testable-Modules-Part-1.aspx
Interestingly enough, he mainly advocates a completely different approach than what I do (in terms of the mock objects and environment, not the methodology) which is definitely worth reading and understanding. One of the largest differences is that he is discussing the MVP (Model View Presenter) pattern, whereas this article discusses more the traditional pattern of DNN module development.
However, the key item that I took away from this blog series was the changes in the way that DNN 5 loads up providers and other components. This is actually all done in the ‘global.asax.vb’ file (which runs at application start). This is one of the reasons many people had DNN 5 upgrade problems (and encountered the ‘componentFactory.Container’ error) – some modules and developers replace the global.asax.vb file with one of their own making – or fail to copy in the new one. This removes the important instantiation of the Component Factory Container, and results in lots of bad errors all over the place. Don’t modify the core code!
The end result for the iFinity.DNN.Utilities is that the startup code had to change. This was done by copying the relevant startup code from the global.asax.vb file, which creates the container and loads the components for the various types of providers supported in DNN. However, not all the various components are loaded : some are commented out in the ‘InstallComponents’ list. These can be re-installed, but you’ll also need to reference the matching DLL from the DotNetNuke \bin directory. For example, if you enable the ‘friendlyUrl’ provider, you’ll need to copy in and reference the matching Friendly Url Provider, as configured in your app.config ‘friendlyUrl’ section.
However, as an upshot of this, the previous problems I had encountered with the BuildManager.GetType() calls in DNN 4.8.0 have disappeared. So this code has been removed from the DNNUnitTest class, and replaced with the aforementioned DNN 5 method.
Visual Studio 2008 Web Context / Host Adapter
Within Visual Studio 2008 Unit Testing, there is a new feature for running Unit Tests in the context of an ASP.NET website. This solution doesn’t take advantage of this : primarily because it’s an evolution of my work done in Visual Studio 2005, which didn’t have this feature in the Unit Testing. Instead, this creates a mock HttpContext to host the DNN application. The end result is similar, although I suspect the Visual Studio version would probably be superior. If time and inclination permits, I may investigate this in the future and modify the DNNUnitTest code to match.
Recommendations for Testing Module Code
Hopefully you have managed to setup the example test project and have got the code running. You can step through the individual tests and investigate how it all works. About now is the time you should start thinking about how to adapt this to your projects. If you develop your modules in a similar way, then you can just remove the ‘iFinity.Example’ module, and replace with your own project. Or, more realistically, either include the iFinity.DNN.Utilities module in with your existing project solution, or just build and reference the DLL once you have it working. You can then add a test project to your solution, copy across the various components of the example test, and start writing your tests.
In the example module tests, you’ll see there is a test for each property set/get. You may think this is overkill, but you’d be surprised how many times this will catch a small but lethal bug from a mistyped property accessor. Also, in this example module, you’ll see that a flag is kept over whether the data in the object has changed since the last check. The logic determines if this code is running correctly.
You’ll also see that the module CRUD code is tested against the database. This is an obvious choice, but don’t forget all the other bits and pieces your module may do, such as reviewing saving/retrieving module settings, generating Urls, creating emails : you should test as much as you can.