Unit Testing Specs using BBMetalWeb

By: David Hodge

Posted on: March 15, 2012

Unit testing isn’t the most exciting thing to do, but it is essential in any given application.  Unless you have perfect requirements and developers, you are going to have to refactor or add new code to existing code.  The purpose of unit testing is to isolate a piece of code and to determine if it is acting exactly as you expect it to.  Unit tests should both include positive and negative tests.  This article is intended for a developer audience and will illustrate one method of unit testing the Infinity framework.

Infinity has no hard-strict method for unit testing.  This makes sense and is inline with the goal of the platform to act as an aid to development as opposed to strict way.  That being said, this method isn’t that hard and true way to unit test but just an example of one.  The important thing is defining your process and goals for unit testing and laying the framework for your developers.  That being said, I am going to use the combination of MSBuild, BBMetalWeb (found in the SDK Tools) and Visual Studio Team Test as my framework.  The framework will need the following items to make it work.

A simple way to generate and compile BBMetalWeb code.

A separate empty database on my local development BBEC instance.

A common set of methods used in my unit test code to help with assertion and cleanup.

The example I am going to show you is from the FoodBank sample catalog project found here.  The first thing I want to do is create a MSBuild project file that can generate and compile BBMetalWeb code.  This could be done in many ways (batch file, straight through command line…) but I choose MSBuild since it will fit well into most build environments.  The Blackbaud.AppFx.Platfrom.BuildTasks.dll found in the SDK already has a task for BBMetalWeb so I just need to set some properties and create some targets.  Here is my project file.

<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.microsoft.com/developer/msbuild/2003    ./Schemas/Microsoft.Build.xsd" ToolsVersion="3.5">

    <PropertyGroup>

        <BuildVirtualDirectory Condition="$(BuildVirtualDirectory)==''">bbappfx.firebird</BuildVirtualDirectory>

        <SelectedDB Condition="$(SelectedDB)==''">BBInfinity_Phoneix2</SelectedDB>

        <SolutionRoot Condition="$(SolutionRoot)==''">$(MSBuildProjectDirectory)\..\DLLReferences</SolutionRoot>

        <BBMetalWebPath Condition="$(BBMetalWebPath)==''">C:\Infinity\DEV\Firebird\Blackbaud.AppFx.SDK\BBMetalWeb\bin\BBMetalWeb.exe</BBMetalWebPath>

    </PropertyGroup>

    <UsingTask AssemblyFile="$(MSBuildProjectDirectory)\Blackbaud.AppFx.Platform.BuildTasks.dll" TaskName="BBMetalWeb"/>

    <Target Name="BuildAndCompileBBMetalWebCode" DependsOnTargets="GenerateBBMetalWebCode;CompileBBMetalWebCode" />

    <Target Name="GenerateBBMetalWebCode">
        <BBMetalWeb Exe="$(BBMetalWebPath)"
                          CatalogPath="$(SolutionRoot)"
                          WsUri="http://localhost/$(BuildVirtualDirectory)/AppFxWebService.asmx"
                          DB="$(SelectedDB)"
                          TargetPath="$(SolutionRoot)\BBMetalWeb"
                          BuildLocation="$(SolutionRoot)"
                          UseCodeFormatting="false"
                          SingleCatalog=""
                          VersionString=""
        />

    </Target>

    <Target Name="CompileBBMetalWebCode">
        <CreateItem Include="$(SolutionRoot)\BBMetalWeb\**\*.vbproj">
            <Output TaskParameter="Include" ItemName="BBMetalWebProjects"/>
        </CreateItem>

        <MSBuild
            Projects="@(BBMetalWebProjects)"
            Properties="OutputPath=$(SolutionRoot)"
        />
    </Target>
</Project>

This project file is not plug-and-play but hopefully you can see what I am trying to accomplish. Using the 2 targets, I am generating BBMetalWeb code and then compiling it into DLLs that end up back in my common DLL folder. Not everyone has the same project / folder structure so I don’t want to get into specifics around the setup. Setting it all up can be cumbersome but once it is done, you have a point and click (or point and type here) way to regenerate the BBMetalWeb code for your projects.Now that I have my BBMetalWeb libraries generated, I am going to use them to create my unit tests. First thing is to make a project for my unit test which I called Blackbaud.CustomFx.FoodBank.Catalog.UnitTests. I added references to my BBMetalWeb library, the WebAPI library and System.Web.Services which are needed. As a note, the libraries that these references need i.e. XmlTypes and Serializers can all be found in the SDK.

image

Now that I have my project setup, I want to build some common code that will be needed for all my tests.  I am going to put everything into one project but this could be factored out into a common project if you choose too.  I will need a common way to get the AppFxWebService provider which is my service endpoint to my local development application that points to my unit testing database.  Since I want to be able to run these tests repeatedly, I will need a way to cleanup after them.  To accomplish this, I made 2 common classes.  One which will serve as a base class for all unit tests and another which will be a shared helper class that has common properties and functions.

Imports Blackbaud.AppFx.WebAPI
Friend NotInheritable Class Common

    Private Sub New()
    End Sub

    Private Shared _webServiceProvider As AppFxWebServiceProvider
    Public Shared ReadOnly Property WebServiceProvider() As AppFxWebServiceProvider
        Get
            If _webServiceProvider Is Nothing Then
                ' NOTE: You will want to develop a common way to fetch these values since they vary per system
                _webServiceProvider = New AppFxWebServiceProvider(url:="http://localhost/bbappfx.firebird/AppFxWebService.asmx", Database:="BBInfinity_Phoneix2", ApplicationName:="UnitTests")
            End If
            Return _webServiceProvider
        End Get
    End Property

    Private Shared _changeAgentId As Guid?
    Public Shared Function ChangeAgentID() As Guid
        If Not _changeAgentId.HasValue Then
            PopulateUsers()
        End If
        Return _changeAgentId.Value
    End Function

    Private Shared _appUserId As Guid?
    Public Shared Function AppUserID() As Guid
        If Not _appUserId.HasValue Then
            PopulateUsers()
        End If
        Return _appUserId.Value
    End Function

    Private Shared Sub PopulateUsers()
        Dim request = WebServiceProvider.CreateRequest(Of ServiceProxy.CurrentUserInfoGetRequest)()
        Dim reply = WebServiceProvider.Service.CurrentUserInfoGet(request)
        _changeAgentId = reply.ChangeAgentID
        _appUserId = reply.AppUserID
    End Sub

    Public Shared ReadOnly Property SqlConnectionString() As String
        Get
            ' NOTE: You will want to develop a common way to fetch these values since they vary per system
            Return "Data Source=(local);Initial Catalog=BBInfinity_Phoneix2;Integrated Security=true"
        End Get
    End Property

    Public Shared Function GetOpenSQLConnection() As SqlClient.SqlConnection
        Dim con = New SqlClient.SqlConnection(SqlConnectionString)
        con.Open()
        Return con
    End Function

    Public Shared Sub Cleanup()
        Using con = GetOpenSQLConnection()
            Using cmd = con.CreateCommand()
                cmd.CommandText = "delete from dbo.USR_FOODITEM where ADDEDBYID = @CHANGEAGENTID"
                cmd.Parameters.AddWithValue("@CHANGEAGENTID", ChangeAgentID)
                cmd.ExecuteNonQuery()
            End Using
        End Using
    End Sub
End Class

 

<TestClass()>
Public MustInherit Class BaseUnitTest

    Shared Sub New()
        Common.WebServiceProvider.StartSession()
    End Sub

    <TestInitialize()> _
    Public Overridable Sub MyTestInitialize()
        Common.Cleanup()
    End Sub

    <TestCleanup()>
    Public Overridable Sub MyTestCleanup()
        Common.Cleanup()
    End Sub
End Class

I want to point out a few things in the common class.  There are methods to fetch the AppFxWebServiceProvider and SQLConnectionString which are hard-coded in this example.  This isn’t going to scale well in a team environment so I encourage you to come up with a method suited for your project.  I currently have an XML file that defines these values and a common class (NOT SHOW HERE) that pulls them.  You could use the Windows registry, environment variables, etc… whatever works best for you.

The BaseUnitTest is one that I will inherit in my tests.  As you can see it will ensure that the cleanup code is called before and after tests (doing both as a precaution).

Now we get to look at the unit test itself.  I took a basic example in the Food Item Add form.  You will want to define your unit testing strategy that will determine what point should be tested.  For the positive tests, I want to verify I can add with the minimum number of fields and the maximum number of fields. For my negative test, I want to verify my one required field, Name, is required and that I get the appropriate exception.

 

Imports Blackbaud.CustomFx.FoodBank.Catalog.WebApiClient
Imports System.Data.SqlClient

<TestClass()>
Public Class FooBankAddTest
    Inherits BaseUnitTest

#Region "Positive tests"

    <TestMethod(), Owner("David Hodge"), Description("Verify we can add a food bank with minimum field")>
    Public Sub AddFoodItemMinFieldsTest()
        Dim data = New AddForms.FoodItem.FoodItemAddFormData()
        data.NAME = "Potatoes"

        Dim foodItemId = data.Save(Common.WebServiceProvider)

        Using con = Common.GetOpenSQLConnection()
            Using cmd = con.CreateCommand()
                cmd.CommandText = "select NAME from dbo.USR_FOODITEM where ID = @ID"
                cmd.Parameters.AddWithValue("@ID", foodItemId)
                Using reader = cmd.ExecuteReader()
                    If reader.Read() Then
                        Assert.AreEqual(data.NAME, reader.GetString(reader.GetOrdinal("NAME")), "Incorrect food item name")
                    End If
                End Using
            End Using
        End Using
    End Sub

    <TestMethod(), Owner("David Hodge"), Description("Verify we can add a food bank with maximum field")>
    Public Sub AddFoodItemMaxFieldsTest()
        Dim data = New AddForms.FoodItem.FoodItemAddFormData()
        data.NAME = "Potatoes"
        data.WEIGHT = 3.5D
        data.LOWINVENTORYTHRESHOLD = 50
        data.DESCRIPTION = "A bag of Idaho potatoes"
        data.CURRENTCOST = 4.5D

        Dim foodItemId = data.Save(Common.WebServiceProvider)

        Using con = Common.GetOpenSQLConnection()
            Using cmd = con.CreateCommand()
                cmd.CommandText = "select NAME,WEIGHT,LOWINVENTORYTHRESHOLD,DESCRIPTION,CURRENTCOST from dbo.USR_FOODITEM where ID = @ID"
                cmd.Parameters.AddWithValue("@ID", foodItemId)
                Using reader = cmd.ExecuteReader()
                    If reader.Read() Then
                        Assert.AreEqual(data.NAME, reader.GetString(reader.GetOrdinal("NAME")), "Incorrect food item name")
                        Assert.AreEqual(data.WEIGHT, reader.GetDecimal(reader.GetOrdinal("WEIGHT")), "Incorrect food item weight")
                        Assert.AreEqual(data.LOWINVENTORYTHRESHOLD, reader.GetInt16(reader.GetOrdinal("LOWINVENTORYTHRESHOLD")), "Incorrect food item low inventory threshold")
                        Assert.AreEqual(data.DESCRIPTION, reader.GetString(reader.GetOrdinal("DESCRIPTION")), "Incorrect food item description")
                        Assert.AreEqual(data.CURRENTCOST, reader.GetDecimal(reader.GetOrdinal("CURRENTCOST")), "Incorrect food item current cost")
                    End If
                End Using
            End Using
        End Using
    End Sub

#End Region

#Region "Negative test"
    <TestMethod(), Owner("David Hodge"), Description("Verify a Name is required for a food item"), ExpectedException(GetType(AppFx.WebAPI.AppFxWebServiceException))>
    Public Sub VerifyFoodItemNameRequired()
        Dim data = New AddForms.FoodItem.FoodItemAddFormData()

        data.Save(Common.WebServiceProvider)
    End Sub
#End Region
End Class

Now we have our unit tests for our food item add form that we can run per build to verify we haven’t created any breaking changes.  Again, the Infinity framework doesn’t provide any direct way of unit testing specs but does provide many indirect tools, i.e. via BBMetalWeb to help with the process.  I could of easily accomplished this using just the Blackbaud.AppFx.WebAPI library using the standard request / reply objects.  That would take away the code generation steps but make the unit test code lengthier.  You could add more common code or have your negative tests go as far as verifying the AppFxWebServiceExcption comes back with the right metadata error information.  The way you go about unit testing isn’t as important as developing the process and the standard of creating them.

Leave a Reply

Privacy Policy | Sitemap | © 2011 Blackbaud, Inc. All Rights Reserved