This blog post is cross-posted from my personal blog.
In the previous article in this blog series on creating testable modules, I added the Repository class to the Business Layer of our LinksMVP module. In this article I will add the concrete DataService class. As I mentioned in the previous article I propose to use the DAL+ methods.
DataService class
Listing 1 shows the DataService class. It contains two private variables, dataprovider which is a reference to the core DataProvider, and moduleQualifer which is a constant we will use the provide a unique name for the stored procedure.
Listing 1 – The DataService class
|
1: public class DataService : IDataService
2: {
3: private DataProvider dataProvider = DataProvider.Instance();
4: private string moduleQualifier = "LinksMVP_";
5:
6: public IDataReader GetLinks(int moduleId)
7: {
8: return dataProvider.ExecuteReader(String.Concat(moduleQualifier, "GetLinks"), moduleId);
9: }
10: }
|
The GetLinks method is very simple – it calls the core DataProvider’s ExecuteReader method, passing the qualified name of the stroed procedure (ie LinksMVP_GetLinks) and the module Id.
Testing the Data Layer
The Data Layer and the Views are the hardest parts of the module to test. The approach I am going to take in testing the DataService class is to create a test database in code before running the tests and then to tear down the database after the tests have been run.
Listing 2 – The DataServiceTests Class’s Constructor
|
1: public DataServiceTests()
2: {
3: //Create a Container
4: ComponentFactory.Container = new SimpleContainer();
5:
6: // Build the Connection String for the administration stuff (create/drop test DB)
7: adminConnectionString = String.Format("Data Source={0};Integrated Security=True", ServerName);
8:
9: // Build the Connection String for the test
10: connectionString = String.Format("Data Source={0};Integrated Security=True;Initial Catalog={1}",
ServerName, DatabaseName);
11:
12: //Register the Settings for the SqlDataProvider
13: Dictionary<string, string> settings = new Dictionary<string, string>();
14: settings["providerName"] = providerName;
15: settings["providerPath"] = providerPath;
16: settings["connectionString"] = connectionString;
17: settings["databaseOwner"] = databaseOwner;
18: settings["objectQualifier"] = objectQualifier;
19: settings["upgradeConnectionString"] = "";
20:
21: //Register the Settings
22: ComponentFactory.Container.RegisterComponentSettings("DotNetNuke.Data.SqlDataProvider",
settings);
23:
24: //Register the DataProvider with the Container
25: ComponentFactory.Container.RegisterComponentInstance(new SqlDataProvider(false));
26: }
|
In the Test classes constructor method, I create a Container to hold the DotNetNuke Data Provider. This mimics the behaviour of the DotNetNuke Application_Start method, which loads all the providers into the Container. In this case we are not configuring the provider from the web.config class, so we create a Dictionary load all the settings and register the Dictionary in the Container, before we register the SqlDataProvider class itself. We have to do the registration in this order as the SqlDataProvider class’s Constructor expects the settings Dictionary to be loaded.
As with the Repository Tests, where we had to load a Caching Provider, we are now ready to “run” our tests, from a code point of view. However, we still need a database to test with.
Setting up the Database for Testing
The testing framework provides an attribute TestInitialize that allows you to mark a method that should run before all the tests. We will use this Attribute to mark a method that we will use to create the Database, and add the Links Table (see Listing 3).
Listing 3 – The TestInitiliaze Method
|
1: [TestInitialize]
2: public void TestInitialize()
3: {
4: // Connect to the server to create the database
5: using (SqlConnection connection = new SqlConnection(adminConnectionString))
6: {
7: connection.Open();
8:
9: CreateDatabase(connection);
10: }
11:
12: // Connect to the database to create the tables
13: CreateObject(SqlScripts.CreateTable, linksTable);
14: }
|
CreateDatabase (Listing 4) creates the Database using the adminConnectionString (a private constant that is set to the connection string required to connect to your Database Server). The SQL to create the Database is stored in a resource file (Figure 1) and accessed using SqlScripts.CreateDatabase.
Listing 4 – The CreateDatabase Method
|
1: private void CreateDatabase(SqlConnection connection)
2: {
3: // Create the database
4: ExecuteScript(connection, String.Format(SqlScripts.CreateDatabase, DatabaseName));
5:
6: // Verify that it was created
7: TestDatabaseAction(connection, SqlScripts.DatabaseCount, DatabaseName, 1);
8: }
|
Figure 1 – The SqlScripts.resx Resource File
|
|
The script is executed in the helper method ExecuteScript (Listing 5) and then the action is verified using the helper method TestDatabaseAction (Listing 6).
Listing 5 – The ExecteScript Method
|
1: private void ExecuteScript(SqlConnection connection, string sqlScript)
2: {
3: // Create the object
4: using (SqlCommand cmd = connection.CreateCommand())
5: {
6: cmd.CommandText = RemoveTokens(sqlScript);
7: cmd.ExecuteNonQuery();
8: }
9: }
|
Listing 6 – The TestDatabaseAction Method
|
1: private void TestDatabaseAction(SqlConnection connection, string sqlScript,
string objectName, int count)
2: {
3: using (SqlCommand cmd = connection.CreateCommand())
4: {
5: if (!String.IsNullOrEmpty(objectName))
6: sqlScript = String.Format(sqlScript, objectName);
7: cmd.CommandText = sqlScript;
8: Assert.AreEqual(count, cmd.ExecuteScalar());
9: }
10: }
|
In a similar way CreateObject (Listing 7) uses the Database connection string, which is again defined as a private constant to run a script that adds a single object to the newly created Database. We again verify that the expected object was in fact created.
Listing 7 – CreateObject Method
|
1: private void CreateObject(string sqlScript, string objectName)
2: {
3: // Connect to the database to create the objects
4: using (SqlConnection connection = new SqlConnection(connectionString))
5: {
6: connection.Open();
7:
8: // Create the object
9: ExecuteScript(connection, sqlScript);
10:
11: // Verify that it was created
12: TestDatabaseAction(connection, SqlScripts.ObjectCount,
13: String.Concat(objectQualifier, objectName), 1);
14: }
15: }
|
Before writing our tests we can confirm that the Database gets created and the Links table is added by running the tests in the Test Runner. Our TestDatabaseAction method contains Assert calls to make sure that what we expected did in fact happen.
The GetLinks DataService Test
We are now ready to write our GetLinks Test (see Listing 8)
Listing 8 – The GetLinks DataService Test
|
1: [TestMethod]
2: public void DataService_Should_Retrieve_All_Links_When_ModuleId_Is_Valid()
3: {
4: // Create the Stored procedure to test
5: CreateObject(SqlScripts.GetLinks, getLinks);
6:
7: // Add Links to Database
8: using (SqlConnection connection = new SqlConnection(connectionString))
9: {
10: connection.Open();
11:
12: // Run the scripts
13: for (int i = 1; i < 3; i++)
14: {
15: string sqlScript = RemoveTokens(SqlScripts.AddLink);
16: string moduleId = MockHelper.ValidModuleId.ToString();
17: string linkUrl = String.Format(MockHelper.LinkURL, i.ToString());
18: string title = String.Format(MockHelper.Title, i.ToString());
19: ExecuteScript(connection, String.Format(sqlScript, moduleId,
20: linkUrl, title)
21: );
22: }
23:
24: // Verify that the links were added
25: TestDatabaseAction(connection, RemoveTokens(SqlScripts.LinksCount), "", 2);
26: }
27:
28:
29: //Create the DataService
30: DataService dataService = new DataService();
31:
32: //Call the GetLinks method
33: IDataReader dataReader = dataService.GetLinks(MockHelper.ValidModuleId);
34: int records = 0;
35: while (dataReader.Read())
36: {
37: records += 1;
38: }
39:
40: dataReader.Close();
41:
42: //Assert that the count is 2
43: Assert.AreEqual<int>(2, records);
44: }
|
In line 4 of Listing 8 we create the GetLinks stored procedure, lines 8 – 26 create two links and add them to the Links table. In line 30 we create an instance of our DataService class and in line 33 we call the GetLinks method. Lines 34 – 40 analyze the IDataReader returned by the GetLinks method and finally in line 43 we assert that the number of records returned was 2 – the number we added above. When we run this test it should pass.
Database Cleanup
This isn’t quite the end yet though. We spent quite a lot of effort setting up a clean Database for our testing. One of the Best Practices of Unit Testing is that the system should be left in the same state as it was before the test was run. We therefore need to delete our test Database.
As with test setup, the testing framework provides an attribute – TestCleanup – which can be applied to a method that will be run by the framework after all the tests have completed – regardless of whether they pass or fail. In this method we will add code to drop our test Database.
Listing 9 – The TestCleanup Method
|
1: [TestCleanup]
2: public void TestCleanup()
3: {
4: SqlConnection.ClearAllPools();
5:
6: // Connect to the server
7: using (SqlConnection connection = new SqlConnection(adminConnectionString))
8: {
9: connection.Open();
10:
11: DropDatabase(connection);
12: }
13: }
|
As with the Initialize phase, we use the adminConnectionString to execute SQL to drop the Database (see Listing 10).
Listing 10 – The DropDatabase Method
|
1: private void DropDatabase(SqlConnection connection)
2: {
3: // Drop the database
4: ExecuteScript(connection, String.Format(SqlScripts.DropDatabase, DatabaseName));
5:
6: // Verify that it was dropped
7: TestDatabaseAction(connection, SqlScripts.DatabaseCount, DatabaseName, 0);
8: }
|
Finally, we check that our database was actually dropped.
Now that we have finished the Data Layer for our Get Links feature we have four tests. Figure 2 shows the results of those four tests and Figure 3 shows the Code Coverage.
Figure 2 – Test Results for LinksMVP
|
|
Figure 3 – Code Coverage Results for LinksMVP
|
|
We have four passing tests and 100% code coverage – so we can be fairly confident that everything is working so far.