-
Do you know what unit tests to write and how many?
People aim for 100% Unit Test Coverage but in the real world this is 100% impractical.
Actually it seems that the most popular metric in TDD (Test Driven Development) is to aim
for 100% of methods to be unit tested. However in the real world this goal is rarely, if
ever, achieved. Unit tests are created to validate and assert that public and protected
methods of a class meet an expected outcome based on varying input. This includes both
good and bad data being tested, to ensure the method behaves as expected and returns the
correct result or traps any errors.
Generally, private methods should not have unit tests written for them as they are not
exposed to other objects outside the original class. These private methods are likely to
be refactored (eg. changed, renamed) over time and will require the unit tests to be updated
and this becomes a maintenance nightmare. So how do private methods get tested? Private
methods should be tested by the unit tests on the public and protected methods calling them
and this will indirectly test the private method behaves as intended.
Eg. You would test correct input such as 12/3 = 4 plus bad input such as 12/4 <> 4 and
that 12/0 does not crash the application, and instead a DivideByZero Exception is thrown and
handled gracefully.
Eg. Methods returning a Boolean value need to have both true and false test cases.
Unit tests should be written for:
- Dependencies - e.g. DLLs Run time errors (JIT) - see below
- Dependencies - e.g. Database Schema, Datasets, Web Services -
see below
- Fragile Code - e.g. Regular Expressions - see below
- When errors can be difficult to spot - e.g. Rounding, arithmetic, calculations -
see below
- Performance - e.g. Slow forms, Time critical applications -
see below
Unit tests should not be written:
- When code has been generated from Code Generators eg. SQL database functions Customer.Select,
Customer.Update, Customer. Insert, Customer. Delete)
- When unit tests become bigger than the original function eg. When you know to insert
items into a database in the SetUp to test a function that uses the database
- For Private methods because these will be tested by the public functions calling
them, and they are likely to be change or refactored.
-
|
Test Object |
Recommended Style |
Example |
|
Project Name |
UnitTests |
UnitTests |
|
Test Fixture Name |
[Type]Tests |
OrdersTests, CustomerTests, DeveloperTests |
|
Test Case |
[Function]Test |
ConstructorTest, InsertDataTest, LoginTest |
|
Set Up |
SetUp |
|
|
Tear Down |
TearDown |
|
-

-
Figure: Good Naming for a Unit Test Project (Good)
Sample Code:
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using NUnit.Framework;
using SSW.NetToolKit.BusinessService;
using SSW.NetToolKit.DataAccess;
namespace SSW.NETToolkit.UnitTests
{
/// <summary>
/// Unit Tests Class. Functionally Tests the Business Rules
/// </summary>
[TestFixture]
public class CustomerTests
{
[SetUp]
public void SetUp()
{
// Initialize the tests e.g. add entries
into database
}
[TearDown]
public void TearDown()
{
// Finalize tests e.g. remove entries
that were added in SetUp()
}
[Test]
public void OrderTotalTest()
{
BusinessRules business = new BusinessRules();
decimal calculatedGrandTotal = business.CalculateOrderGrandTotal(
10248 );
// Calculate the grand total for a simple
example order items.
Assert.AreEqual(440, calculatedGrandTotal,
"Calculated grand total didn't match the expected.");
// Example with discounts
calculatedGrandTotal = business.CalculateOrderGrandTotal(
10260 );
Assert.AreEqual(1504.65m, calculatedGrandTotal,
"Calculated grand total didn't match the expected.");
}
[Test]
public void RoundingTest()
{
BusinessRules business = new BusinessRules();
// test round up
Assert.AreEqual(149.03, business.ApplyRounding(149.0282m),
"Incorrect rounding rules applied for round up condition.");
// test round down
Assert.AreEqual(149.02, business.ApplyRounding(149.0232m),
"Incorrect rounding rules applied for round down condition.");
// test no rounding needed
Assert.AreEqual(149.02, business.ApplyRounding(149.02m),
"Incorrect rounding rules applied for no rounding condition.");
// test border condition
Assert.AreEqual(149.02, business.ApplyRounding(149.025m),
"Incorrect rounding rules applied for border condition.");
}
}
-

-
Figure: This rule is consistent with the Visual Studio default
We have a program called SSW .Net Toolkit that implements
this.
Tip: You can create a test project using the Unit Test Wizard: Test > Add New Test
-

-
Figure: Unit Test Wizard 1
-

-
Figure: Unit Test Wizard 2
-
Do you have unit tests outside the project - not inside?
Unit tests should be written outside the project because:
- Separation of duties - the guys writing the unit tests should not have knowledge
of the internals of the project - they should see it as a black box
- Most developers don't want the unit tests deployed - even with conditional compilation
you leave references behind
- The projects will be smaller without the unit tests
-

-
Figure: Bad Structure;
-

-
Figure: Bad Structure - each layer for UnitTests are related to each other, meaning
it's not easy to maintain;
-

-
Figure: Good Structure - each layer for UnitTests are isolated from each other,
meaning it's easier to maintain (adding new project and test, removing a project
and its test cases);
-
Do you write unit tests for Dependencies - e.g. DLLs?
Dependant code is code that relies on other factors like methods and classes inside
a separate DLL. Because of the way the .NET works assemblies are loaded as required
by the program (this is what we call the JIT compiler). Thus, when a DLL goes astray,
you will only find out at run time when you run a form/function that uses that DLL.
These run time errors can occur when you have not packaged DLLs in your release
or if the versions are incompatible. Such errors cause the following exceptions:
- An unhandled exception ("System.IO.FileNotFoundException") occurred in
SSW.NETToolkit.exe.
- System.IO.FileLoadException The located assembly's manifest definition with name
'SSW.SQLDeploy.Check' does not match the assembly reference.
These errors can be fixed by writing a Unit Test to check all referenced assemblies
in a project.
Sample Code:
[Test]
public void ReferencedAssembliesTest()
{
// Get the executing assembly
Assembly asm = Assembly.GetExecutingAssembly();
// Get the assemblies that are referenced
AssemblyName[] refAsms = asm.GetReferencedAssemblies();
// Loop through and try to load each assembly
foreach( AssemblyName refAsmName in refAsms)
{
try
{
Assembly.Load(refAsmName);
}
catch(FileNotFoundException)
{
// Missing assembly
Assert.Fail(refAsmName.FullName + " failed to load");
}
}
}
-
Figure: This code is a unit test for checking that all referenced assemblies are
able to load.
We have a program called SSW .Net Toolkit that implements
this.
-
Do you *not* write unit tests for Database Dependencies
- e.g. Database Schema, Datasets?
If we did our Data Access layer manually we should be testing this. Instead we recommend
using:
- LINQ (VS2008) or Code Generators (VS2005)
- Use
a _regenerate.bat
- Add a unit test to create the database and check that "reconcile" works
We have a program called SSW SQL Deploy that implements
this.
-

-
Figure: The dataset and the database schema are not consistent (Bad)
-

-
Figure: The dataset and the database schema are consistent (Good)
As long as you have a _regenerate.bat file your schema will not get out of sync.
-
With web services you should:
- Use a _regenerate.bat to regenerate the proxy classes (See our
Rules To BetterWindows Forms for more information.)
- Compare old WSDL with new WSDL to pick up changes and email the diff file to developers
- Minimum Tests - Call the web service to check it’s alive
- Performance Test - If it takes longer than 4 seconds to call that methods – it fails
-
Fragile code is code that often breaks and are difficult to understand. An example
of this is with Regular Expressions.
When using regular expressions, because of their rather cryptic syntax, should always
be associated with at least two sample tests - one that passes the regular expression
and one that fails. These regular expressions should be stored in a central location,
i.e. a database or a class file.
A Unit Test should be written such that it loops through each of these regular expressions
and tests it against the supplied test cases.
Sample Code:
public bool ValidateEmail(string txtEmail)
{
RegexList regexList = new RegexList();
Regex r = new Regex("^[\w-]+(?:\.[\w-]+)*@(?:[\w-]+\.)+[a-zA-Z]{2,7}$") // checks email
Match m = r.Match(txtEmail);
return m.Success;
}
-
Figure: A function to validate email addresses
[Test]
public void GoodEmailRegexTest()
{
string txtEmail = "joeblogs@blogsville.com";
Assert.IsTrue(ValidateEmail(txtEmail));
}
-
Figure: Tests a good case
[Test]
public void BadEmailRegexTest()
{
string txtEmail = "joeshmo.com.au";
Assert.IsFalse(ValidateEmail(txtEmail));
}
-
Figure: Tests a bad case
-
By difficult to spot errors we mean errors that do not give the user a prompt that
an error has occurred. Things such as: Arithmetic, Rounding or Calculations should
have unit tests written for them.
Sample Code:
DataAccess |Utilities.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace DataAccess {
public class Utilities {
// A business rule – it should be unit tested
public decimal CalculateTotal(List items) {
decimal total = 0.0M;
foreach (MyItem i in items) {
total += i.UnitPrice * (1 - i.Discount);
}
return total;
}
}
}
-
Figure: Function to calculate a total for a list of items
For a function like this, it might be simple to spot errors when there are one or
two items, but if you were to calculate the total for 50 items, then the task of
spotting an error isn't so easy. That's why a unit test should be written so that
you know when the function doesn't work.
Sample Test:
DataAccess.Tests | UtilitiesTest.cs
using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
namespace DataAccess.Tests {
[TestFixture()]
public class UtilitiesTests {
[Test()]
public void CalculateTotalSimpleTest() {
List<myitem> items = new List<MyItem>();
items.Add(new MyItem(12.50M));
items.Add(new MyItem(4.75M));
items.Add(new MyItem(9.05M));
items.Add(new MyItem(6.55M));
items.Add(new MyItem(20.12M));
decimal expected = 52.97M;
Utilities u = new Utilities();
decimal actual = u.CalculateTotal(items);
Assert.AreEqual(expected, actual);
}
[Test()]
public void CalculateTotalWithDiscountTest() {
List<myitem> items = new List<myitem>();
items.Add(new MyItem(12.50M, 0.1M));
items.Add(new MyItem(4.75M));
items.Add(new MyItem(9.05M));
items.Add(new MyItem(6.55M));
items.Add(new MyItem(20.12M));
decimal expected = 51.72M;
Utilities u = new Utilities();
Assert.IsNotNull(u);
decimal actual = u.CalculateTotal(items);
Assert.AreEqual(expected, actual);
}
}
}
DataAccess | MyItem.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace DataAccess {
public class MyItem {
public MyItem(decimal unitPrice) {
_unitPrice = unitPrice;
}
public MyItem(decimal unitPrice, decimal discount) : this(unitPrice) {
_discount = discount;
}
private decimal _unitPrice;
private decimal _discount;
public decimal Discount {
get { return _discount; }
set { _discount = value; }
}
public decimal UnitPrice {
get { return _unitPrice; }
set { _unitPrice = value; }
}
}
}
-
Figure: Test calculate total by checking something we know the result of. (Note:
it doesn’t need a failure case because it isn’t a Regex.)
We have a program called SSW .Net Toolkit that implements
this.
-
Typically there are User Acceptance Tests that need to be written to measure the
performance of your application. As a general rule of thumb, Forms should not load
in more than 4 seconds. This can be done using
NUnit.
Sample Code:
public abstract class FormTestBase<F>
where F : Form, new()
{
protected TimeSpan _ExpectedLoadTime = TimeSpan.FromSeconds(4);
[Test]
public void LoadTest()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
F testForm = new F();
testForm.Show();
testForm.Close();
stopwatch.Stop();
Console.WriteLine("Form [{0}] took {1:#,##0.0} seconds to open", typeof(F), stopwatch.Elapsed.TotalSeconds);
Assert.IsTrue(stopwatch.Elapsed < _ExpectedLoadTime,
string.Format("Loading time ({0:#,##0.0} seconds) exceed the expected time ({1:#,##0.0} seconds).",
stopwatch.Elapsed.TotalSeconds, _ExpectedLoadTime.TotalSeconds));
}
}
[TestFixture]
public class LoginFormTests : FormTestBase<LoginForm>
{
}
[TestFixture]
public class MainFormTests : FormTestBase<MainForm>
{
}
-
Figure: This code tests that the LoginForm and MainForm load in under 4 seconds
We have a program called SSW .Net Toolkit that implements
this.
-
You need to be able to use a right click menu to run the unit tests. You have two
options:
- Resharper (Recommended)
- Test Driven .NET
Both integrate with Visual Studio, NUnit and other programs to make testing your
software easy.
-

-
Figure: Run your unit tests from your right click menu using Resharper (Recommended)
-

-
Figure: Run your unit tests from your right click menu using Test Driven .NET
-
In general you should always be looking to simplify your code (e.g. heavily nested
case statements). As a minimum look for the most complicated method you have and
check that it needs simplifying.
In VS 2008, there is inbuilt support for Cyclomatic Complexity analysis.
- Go to Developer > Code Metrics > Generate for Solution
-

-
Figure: Cyclomatic Complexity analysis tool
- Look at the largest Cyclomatic Complexity number and refactor.
-

-
Figure: Results from Cyclomatic analysis these metrics give an indication on how
complicated functions are.
-
Menu - Do you have a standard 'Help' menu that includes
a way to run your unit tests?
Thanks Adam for coming to our user group in Boston and presenting your session “Rules
to Healthier Code”
While I had used most of your best tips I’d never thought to put them in concert
as you have. In concert I mean together.. I have used and demo’d NUNIT, FxCop. MBUnit
and almost the full range of third party tools you suggest. I tend to use them when
I need to fix a particular problem.
BUT I never thought to use all of them together. Like your tip to include NUnit
with the finished product! Just brilliant! It still makes me laugh at the simplicity
of it. I get the flash of a cool validation tool and user validation for the work
I’ve already done in NUnit. Its good marketing, good salesmanship and good engineering!
Its just elegant.. I might include it with my setup as part of the install! “Click
here to validate your install” It’ll force me to be more diligent about NUnit, but
now there is a better reason for the diligence.
Again thanks for the great presentation Wednesday. I’ll be re-working the concept
into my own UG programs here in New Hampshire.
Pat Tomey
www.4square.net
Your standard help menu should include an option to run your Unit Tests. Everybody
knows the importance of Unit tests for the middle tier. However, Unit Tests are
also important to capture problems that occur on other peoples' machines so that
users can perform a quick check when a product is not behaving correctly. This is
important for troubleshooting during support calls and enables your customers to
do a Health Check on the product.
And yes, there are many tests that can be written that will pass on the developers
PC – but not on the users PC. e.g. Ability to write to a directory, missing dlls,
missing tables in the schema etc.
Note: Adding this option requires you to include NUnit in your setup.exe (See Include all the files needed
in our Wise Standard)
-

-
Figure: Standard Help menu should give you an option to Run Unit Tests to check
the users' environment (Good)
-
-
Figure: Obviously the red indicates that there is a problem with a Unit Test (Good)
We have a rule Do you know the six items
every Help menu needs?
We have a program called SSW .Net Toolkit that implements
this.
-
You can also write unit tests for Javascript code. With AJAX becoming the norm,
more javascript is being written, and since there isn’t good intelligense support
for it, it is prone to errors.
You can write unit tests for javascript using:
- JsUnit
-

-
Figure: TestRunner
- AJAX toolkit test harness
-

-
Figure: AJAX toolkit test harness
-
Unit tests shouldn't only for developers, it should be able to run on customer's
machines as well. Based on this consideration, we still NUnit in order to allow
our users to run available tests on their machine. This will help us collect more
useful information on client machines and give our products more confidence and
professional look.
As NUnit provides a great UI for this purpose, we recommend you to use NUnit even
you've already been working on VSTS.
-

-
Figure: NUnit UI
-
Did you know Visual Studio Team System does the same thing (but
we don't recommend it)?
Visual Studio Team System has unit testing integrated into the IDE. There is a new
Test View window and a Test Results window. You can run the unit tests from the
right click menu in Visual Studio Team System.
-

-
Figure: New Test View window (run tests from right click)
-

-
Figure: New Test Results window
You can write unit tests much the same way as in NUnit with a few changes in syntax:
|
Test Attribute |
NUnit |
Visual Studio Team System |
|
Test Fixture |
[TestFixture] |
[TestClass] |
|
Set Up |
[SetUp] |
[TestInitialize] |
|
Tear Down |
[TearDown] |
[CleanUp] |
|
Test |
[Test] |
[TestMethod] |
VSTS is a great integrated toolset and there are many features of it you need. However
because there is no existing GUI to run these tests outside of VSTS (like you can
with nUnit) there is no way of having this menu option in your application.
Therefore we *don't recommend* it if you also want
to Run Unit Tests from the Help Menu
-
We recommend you have test data for your database configuration and reconcile form
rather than testing it by opening the UI and doing the configuration manually.
-

-
Figure 1: Get user's confirmation
-

-
Figure 2: Create test database
-

-
Figure 3: Reconcile
Sample Code:
/// <summary>
/// Create a temp database and do reconcile then drop it
///</summary>
///<param name="Config">DBConfig Config</param>
///<returns>string</returns>
public static string ConfigAndReconcileTester(DBConfig Config)
{
try {
_deployForm = new DatabaseDeployForm(Config);
_deployForm.IsATest = true;
if(_deployForm.ShowDialog() != DialogResult.OK)
{
return "Failed to create test database";
}
Config.IsReconcile = true;
DialogResult reconciled = new
SqlDeployHelper(Config).BeginProcess();
if(reconciled == DialogResult.OK)
{
return SUCCESS_INFO;
}
return "Reconciliation failed";
}
finally
{
DropTestDB(Config);
}
}
Imports System
Imports NUnit.Framework
Imports SSW.Framework.Data
Imports SSW.Framework.UnitTests
Import SSW.Framework.Data.SqlServer.WindowsUI
Imports SSW.SQLAuditor.WindowsUI
Namespace Functional
<TestFixture()> _
Public Class DataBaseConfigureTests
Private Const STR_SSWSQLAuditorNorthwindTest As String = "SSWSQLAuditorNorthwindTest"
Private config As DBConfig = New DBConfig()
<SetUp()> _
Public Sub Setup()
'configurations for database deploy form
config.ConnectionBuilder.ConnectionString = String.Empty
config.CreateScriptsPath = System.IO.Path.Combine(Application.StartupPath + "\..\..\", _
"DatabaseSQLScripts")
config.UpgradeScriptsPath = config.CreateScriptsPath
config.SampleScriptsPath = System.IO.Path.Combine(config.CreateScriptsPath, "Samples")
config.NewDatabaseName = STR_SSWSQLAuditorNorthwindTest
config.IsDatabaseNameEnforced = True
config.ShouldCreateSamples = True
config.IsNew = True
config.IsSampleDatabaseNameEnforced = True
config.SampleDatabaseName = STR_SSWSQLAuditorNorthwindTest
config.DatabaseNamePlaceholder = "[DatabaseName]"
End Sub
<Test()> _
Public Sub ConfigureTest()
Dim testResult As String = String.Empty
'get user's confirmation to continue test
If MessageBox.Show("This will create the sample database and reports and then delete them." _
+ Environment.NewLine + " Do you want to continue?", "Continue", MessageBoxButtons.OKCancel, _
MessageBoxIcon.Exclamation) = DialogResult.Cancel Then
Return
End If
testResult = DatabaseConfigTester. ConfigAndReconcileTester(config)
Assert.AreEqual(testResult, DatabaseConfigTester.SUCCESS_INFO, testResult)
End Sub
<TearDown()> _
Public Sub RestoreToDefault()
End Sub
End Class
End Namespace
-
We also recommend you have test data for your reporting service configuration. However,
before configuring the reporting service, you should specify a data source for the
reports using your database deploy form.
-

-
Figure: Reporting service config test
Sample Code:
///
<summary>
/// Properties ApplicationName, ReportDirectoryName,
ReportDirectoryName, ///
ReportRdlFolderPathAlternative, ReportManagerUrl and ReportServerWebServiceUrl /// in rsConfig should
be configed before start testing. /// </summary> ///
<param name="dbConfig"></param> ///
<param name="rsConfig"></param>
public static void
StartTest(DBConfig dbConfig,RSConfig rsConfig) { (new DatabaseDeployForm(dbConfig)).ShowDialog();
//get a data source from database deploy form rsConfig.DataSourceConnectionString
= dbConfig.ConnectionBuilder.ConnectionString;
rsConfig.DataSourceName =
dbConfig.ConnectionBuilder.DatabaseName; //pop up a reporting
service config form PublishReportsForm prForm
= new PublishReportsForm(new ReportSetupControl(),rsConfig); prForm.IsAUnitTest =
true; Assert.AreEqual(prForm.ShowDialog(),DialogResult.OK,"Cancled by user"); } }
usingSystem;usingSystem.Windows.Forms;
usingSystem.Collections.Generic;
usingSystem.Text;
usingNUnit.Framework;
usingSSW.Framework.Data;
usingSSW.Framework.UnitTests;
usingSSW.Framework.Data.SqlServer.WindowsUI;
usingSSW.Framework.ReportingServices.WindowsUI;
usingSSW.LinkAuditor.Common;
namespace
SSW.LinkAuditor.UnitTests.Functional { [TestFixture]
publicclassReportingServiceConfigTests {
privateconststring STR_SSWLinkAuditor = "SSWLinkAuditor"; private DBConfig _dbConfig = new
DBConfig(); RSConfig _rsConfig = new
RSConfig(); [SetUp] public
void SetUp()
{ //configurations for database deploy form
_dbConfig.ConnectionBuilder.ConnectionString = String.Empty; _dbConfig.CreateScriptsPath
= System.IO.Path.Combine(Application.StartupPath
+ @"\..\Database", "Create Scripts"); _dbConfig.UpgradeScriptsPath
= _dbConfig.CreateScriptsPath;
_dbConfig.SampleScriptsPath =
_dbConfig.CreateScriptsPath; _dbConfig.NewDatabaseName
= STR_SSWLinkAuditor; _dbConfig.IsDatabaseNameEnforced
= false; _dbConfig.ShouldCreateSamples
= false; _dbConfig.IsNew
= true; _dbConfig.IsSampleDatabaseNameEnforced
= false; _dbConfig.SampleDatabaseName
= STR_SSWLinkAuditor;
_dbConfig.DatabaseNamePlaceholder = "[DatabaseName]"; //configurations for reporting
service config form _rsConfig.ApplicationName
= "SSW Link Auditor"; _rsConfig.ReportDirectoryName
= _rsConfig.ApplicationName
+ " Reports"; _rsConfig.ReportRdlFolderPath
= AppDomain.CurrentDomain.BaseDirectory +
@"\Report\RS2000"; _rsConfig.ReportRdlFolderPathAlternative
= AppDomain.CurrentDomain.BaseDirectory +
@"\Reports\RS2005"; _rsConfig.ReportManagerUrl
= "http://localhost/Reports"; _rsConfig.ReportServerWebServiceUrl
= "http://localhost/reportserver/ReportService.asmx"; }
[Test] public
void ConfigTest()
{ ReportingServiceConfigTester.StartTest(_dbConfig,_rsConfig);
} } }
-
If you store your URL references in the application settings, you can create unit
tests to validate them.
-
-
Figure: URL for link stored in application settings
Sample Code: How to test the URL
[Test]
public void urlRulesToBetterInterfaces()
{
HttpStatusCode result = WebAccessTester.GetWebPageStatusCode(Settings.Default.urlRulesToBetterInterfaces);
Assert.IsTrue(result == HttpStatusCode.OK, result.ToString());
}
Sample Code: Method used to verify the Page
public class WebAccessTester
{
public static HttpStatusCode GetWebPageStatusCode(string url)
{
HttpWebRequest req = ((HttpWebRequest)(WebRequest.Create(url)));
req.Proxy = new WebProxy();
req.Proxy.Credentials = CredentialCache.DefaultCredentials;
HttpWebResponse resp = null;
try
{
resp = ((HttpWebResponse)(req.GetResponse()));
if (resp.StatusCode == HttpStatusCode.OK)
{
if (url.ToLower().IndexOf("redirect") == -1 && url.ToLower().IndexOf(resp.ResponseUri.AbsolutePath.ToLower()) == -1)
{
return HttpStatusCode.NotFound;
}
}
}
catch (System.Exception ex)
{
while (!(ex == null))
{
Console.WriteLine(ex.ToString());
Console.WriteLine("INNER EXCEPTION");
ex = ex.InnerException;
}
}
finally
{
if (!(resp == null))
{
resp.Close();
}
}
return resp.StatusCode;
}
}
-
If there are complex logic evaluations in your code, we recommend you isoloate them
and write unit tests for them.
Take this for example:
-
while ((ActiveThreads > 0 || AssociationsQueued > 0) && (IsRegistered || report.TotalTargets <= 1000 )
&& (maxNumPagesToScan == -1 || report.TotalTargets < maxNumPagesToScan) && (!CancelScan))
-
Figure: This complex logic evaluation can't be unit tested.
Writing a unit test for this piece of logic is virtually impossible – the only time
it is executed it during a scan, and there are lots of other things happening at
the same time meaning the unit test will often fail and you won’t be able to identify
the cause anyway.
We can update this code to make it testable though …
- Update the line to this:
-
while (!HasFinishedInitializing (ActiveThreads, AssociationsQueued, IsRegistered,
report.TotalTargets, maxNumPagesToScan, CancelScan))
-
Figure: Isolate the complex logic evaluation.
We are using all the same parameters – however now we are moving the actual logic
to a separate method.
- Now create the method:
private bool HasFinishedInitializing(int ActiveThreads, int AssociationsQueued, bool IsRegistered,
int TotalAssociations, int MaxNumPagesToScan, bool CancelScan)
{
return (ActiveThreads > 0 || AssociationsQueued > 0) && (IsRegistered || TotalAssociations <= 1000 )
&& (maxNumPagesToScan == -1 || TotalAssociations < maxNumPagesToScan) && (!CancelScan);
}
-
Figure: Function of the complex logic evaluation.
The critical thing is that everything the method needs to know is passed in … it
mustn’t go out and get any information for itself and mustn’t rely on any other
objects being instantiated. A good way to enforce this is to make each of your logic
methods static. It has to be completely self contained.
The other thing we can do now is actually go and simplify / expand out the logic
so that it’s a bit easier to digest.
private bool HasFinishedInitializing(int ActiveThreads, int AssociationsQueued, bool IsRegistered,
int TotalAssociations, int MaxNumPagesToScan, bool CancelScan)
{
//Cancel
if (CancelScan)
{ return true; }
//only up to 1000 links if it is not a registered version
if (!IsRegistered && TotalAssociations > 1000)
{ return true; }
//only scan up to the specified number of links
if (MaxNumPagesToScan != -1 && TotalAssociations > MaxNumPagesToScan)
{ return true; }
//not ActiveThread and the Queue is full
if(ActiveThreads <= 0 && AssociationsQueued <= 0)
{ return true; }
return false;
}
-
Figure: Simplify the complex logic evaluation.
The big advantage now is that we can unit test this code easily in a whole range
of different scenarios!
[Test]
public void HasFinishedInitializingLogicTest()
{
Validator validator = new Validator();
//Set up scenario A
int activeThreads = 2;
int associationsQueued = 20;
bool isRegistered = false;
int totalAssociations = 1200;
int maxNumPagesToScan = -1;
bool cancelScan = false;
bool actual = (bool)Reflection.InvokeMethod("HasFinishedInitializing", validator,
new object[] {activeThreads, associationsQueued, isRegistered,
totalAssociations, maxNumPagesToScan, cancelScan});
Assert.IsTrue(actual, "HasFinishedInitializing LogicTest A failed.");
//Set up scenario B
activeThreads = 2;
associationsQueued = 20;
isRegistered = true;
totalAssociations = 1200;
maxNumPagesToScan = -1;
cancelScan = false;
actual = (bool)Reflection.InvokeMethod("HasFinishedInitializing", validator,
new object[] {activeThreads, associationsQueued, isRegistered,
totalAssociations, maxNumPagesToScan, cancelScan});
Assert.IsFalse(actual, "HasFinishedInitializing LogicTest B failed.");
}
-
Figure: Write unit test for complex logic evaluation.
-
If your method is consist of logic and IO, we recommend you isoloate them to increase
the testability of the logic.
Take this for example (and see how we refactor it):
public static List GetFilesInProject(string projectFile)
{
List<string> files = new List<string>();
TextReader tr = File.OpenText(projectFile);
Regex regex = RegexPool.DefaultInstance[RegularExpression.GetFilesInProject];
MatchCollection matches = regex.Matches(tr.ReadToEnd());
tr.Close();
string folder = Path.GetDirectoryName(projectFile);
foreach (Match match in matches)
{
string filePath = Path.Combine(folder, match.Groups["FileName"].Value);
if (File.Exists(filePath))
{
files.Add(filePath);
}
}
return files;
}
- Bad - The logic and the IO are coded in a same method.
While this is a small concise and fairly robust piece of code, it still isn't that
easy to unit test. Writing a unit test for this would require us to create temporary
files on the hard drive, and probably ending up requiring more code than the method
itself.
If we start by refactoring it with an overload, we can remove the IO dependency
and extract the logic further making it easier to test:
public static List<string> GetFilesInProject(string projectFile)
{
string projectFileContents;
using (TextReader reader = File.OpenText(projectFile))
{
projectFileContents = reader.ReadToEnd();
reader.Close();
}
string baseFolder = Path.GetDirectoryName(projectFile);
return GetFilesInProjectByContents(projectFileContents, baseFolder, true);
}
public static List<string> GetFilesInProjectByContents(string projectFileContents, string baseFolder, bool checkFileExists)
{
List<string> files = new List<string>();
Regex regex = RegexPool.DefaultInstance[RegularExpression.GetFilesInProject];
MatchCollection matches = regex.Matches(projectFileContents);
foreach (Match match in matches)
{
string filePath = Path.Combine(baseFolder, match.Groups["FileName"].Value);
if (File.Exists(filePath) || !checkFileExists)
{
files.Add(filePath);
}
}
return files;
}
-
Good - The logic is now isolated from the IO.
The first method (GetFilesInProject) is simple enough that it can remain untested.
We do however want to test the second method (GetFilesInProjectByContents). Testing
the second method is now too easy:
[Test]
public void TestVS2003CSProj()
{
string projectFileContents = VSProjects.VS2003CSProj;
string baseFolder = @"C:\NoSuchFolder";
List<string> result = CommHelper.GetFilesInProjectByContents(projectFileContents, baseFolder, false);
Assert.AreEqual(15, result.Count);
Assert.AreEqual(true, result.Contains(Path.Combine(baseFolder, "BaseForm.cs")));
Assert.AreEqual(true, result.Contains(Path.Combine(baseFolder, "AssemblyInfo.cs")));
}
[Test]
public void TestVS2005CSProj()
{
string projectFileContents = VSProjects.VS2005CSProj;
string baseFolder = @"C:\NoSuchFolder";
List<string> result = CommHelper.GetFilesInProjectByContents(projectFileContents, baseFolder, false);
Assert.AreEqual(6, result.Count);
Assert.AreEqual(true, result.Contains(Path.Combine(baseFolder, "OptionsUI.cs")));
Assert.AreEqual(true, result.Contains(Path.Combine(baseFolder, "VSAddInMain.cs")));
}
-
Good - Different test cases and assertions are created to test the logic.
-
When you encounter a bug in your application you should never let the same bug happen
again. The best way to do this is to write a unit test for the bug.
See our Rules
to Better Email to implement a good 'DONE' email.
-
There are multiple version of NUnit and .NET Framework, the following introduce
to you how to use them correctly.
- if your application was built with .NET Framework 1.1, so NUnit 2.2.0 which was
built with .NET Framework 1.1 is the best choice if you compact it into the installation
package, and then you don't need any additional config - it will auto use .NET Framework
1.1 to reflect your assembly;
- If there is only .NET Framework 2.0 on the client side, how to make it works?
Just add the yellow into nunit-gui.exe.config (it is under the same folder
as nunit-gui.exe), which tell NUnit to reflect your assembly with .NET Framework
2.0;
...
<startup>
<supportedRuntime version="v2.0.50727" />
<supportedRuntime version="v1.1.4322" />
<supportedRuntime version="v1.0.3705" />
<requiredRuntime version="v1.0.3705" />
</startup>
...
- if your application was built with .NET Framework 2.0, then you may get choices:
- NUnit 2.2.7 or higher (built with .NET framework 2.0) (recommended)
Then you don't need any extra configuration for NUnit, just follow the default;
- NUnit 2.2.0 or lower (built with .NET Framework 1.1)
Then you need to add the yellow statement (see above in this section);
-
There are two kinds of errors, coding errors and deployment errors, coding errors should be find during development by compiling or debugging,
while deployment errors should be find by a validation test.
Refer to the following rules for details:
See SSW Rules - Do you have a zsValidate page to make sure your website is healthy?
See SSW Rules - Do you have a Validation Page for your web server?
-
The code below could help you test your send mail code:
-
DotNetOpenMailProvider provider = new DotNetOpenMailProvider();
NameValueCollection configValue = new NameValueCollection();
configValue["smtpServer"] = "127.0.0.1";
configValue["port"] = "8081";
provider.Initialize("providerTest", configValue);
TestSmtpServer receivingServer = new TestSmtpServer();
try
{
receivingServer.Start("127.0.0.1", 8081);
provider.Send("phil@example.com",
"nobody@example.com",
"Subject to nothing",
"Mr. Watson. Come here. I need you.");
}
finally
{
receivingServer.Stop();
}
// So Did It Work?
Assert.AreEqual(1, receivingServer.Inbox.Count);
ReceivedEmailMessage received = receivingServer.Inbox[0];
Assert.AreEqual("phil@example.com", received.ToAddress.Email);
-
Figure: This code could help you validate the send mail code.