Introduction
When I'm starting a new project, I often forget how to start it in a test driven way. Here are some general reminders useful for C# development. Developing with tests is much better than trying to add them retrospectively. You will have less bugs, and more confidence in your code.What is Test-driven?
Test-driven is an approach to developing software. There is no excuse anymore for not writing tests as you develop, there are lots of test frameworks free of charge to use (for example MS Test built into Visual Studio). Best practices dictate every time you develop some code, follow these steps:- Resolve your requirement. Remove any ambiguity, stipulate concisely what you are trying to achieve. Consider error conditions and edge cases, the "what-ifs" ?
- Develop a test. Write a test that will prove the function you are writing works exactly as you want it to. Then, add some tests to make sure if it fails you handle the failure in a graceful manner, nobody wants to use software that bombs out periodically.
- Develop your function. Use comments for any non-obvious code. Use sensible variable and function names.
- Add logging. I use different levels of logging. For example, I add a log entry for every function as an Info, caught exceptions will print an Error. Make sure your error message gives enough information (if possible) for the user to sort the problem out themselves, thus cutting down lots of support calls, and bug reports, etc.
Test Project
Always start your development with a test project, to set one up simply create one, File->New Project->Templates->Visual C#->Test->Unit Test Project.Link the code you want to test into your test projects references.
Creating Test Classes and Tests
All test classes that contain tests should bepublic. Here are some reminders of the MS Test annotations...[TestClass()]- Makes the class a test class[TestMethod]- Marks the function as a test function[Description("Test the set up of command parameters")]- Add a description about the intention of the test[Ignore]- Don't run the test as default. Ignore the test if you want to keep the test code, but only want to test it occasionally. This is especially useful if it leaves a device in a undesired state, i.e., reboot on an ios device, or if the test takes ages to run.
Test Conditions
When testing, use theAssert class. Remember you are making sure something is true. For example, Assert(This is true).Note that you write an exception with what you expect as the first parameter and what you actually got as the second. The arguments are ordered, if you swap them around, you will get false information, i.e., Expected
null, got a valid reference is not the same as Expected a valid reference and got a null!Here are some
Assert functions:IsTrue- Is the statement trueIsFalse- Is the statement not trueAreEqual- Are the two equalIsNull- Is the referencenull
Fail
When testing, try not to be only optimistic, have tests that pass innulls,
files that don't exist, etc. If we hit a Fail assert, then chances are
the code you are testing hasn't thrown an exception or caused what you
expected. Useful for if the code is supposed to throw an exception,
i.e.:
Hide Shrink
Copy Code
[TestMethod]
[Description("Test the pairing of the device")]
public void DeviceTargetConnect(string deviceId)
{
IOSDevice targetDevice = new IOSDevice(Id);
//Dont support callbacks on disconnect
Assert.IsFalse(targetDevice.SupportCB);
//Do support connecting
Assert.IsTrue(targetDevice.SupportConnection);
//Connect to device
try
{
targetDevice.Connect();
//Now make sure we are connected
Assert.IsTrue(targetDevice.IsConnected());
}
catch (Exception exc)
{
Assert.Fail("Connecting to the device failed.
Exception : " + exc.ToString());
}
}
Setting Up and Tearing Down
You can have functions that are called at certain times to set things up, and then to tear them down again.Here are the annotations:
[ClassInitialize] - Gets called when the test suite begins (when the test run begins):
Hide Copy Code
public static void IOSTestSettings(TestContext context)
[ClassCleanup] - Gets called when test suite has finished (when the test class goes out of scope):
Hide Copy Code
public static void CleanIOSTestSettings()
[TestInitialize] - Gets called before each individual test function:
Hide Copy Code
public void StartTest()
[TestCleanup] - Gets called after each individual test function:
Hide Copy Code
public void EndTest()
Note that if you do not declare them as public, they will not get called, and sometimes people put function brackets after the annotation, i.e., [TestCleanup()], but this makes no difference.Print Output TTY
When you want to output any text in Visual Studio, use:
Hide Copy Code
System.Diagnostics.Debug.WriteLine("Hello Dave ");
This will print out your text in the Immediate Window, not the Output window.Temporary Dialogs
Sometimes, you want to run a test and make sure other threads are behaving as expected. There are two approaches to setting up the code so you can examine what is going on.Sleep
If you need an operation to finish before the next line is executed and you cannot get a notification from the system you are calling, although not perfect, sleep is the best option.
Hide Copy Code
System.Threading.Thread.Sleep(TimeToWaitForSomething);
Make sure you don't put magic numbers, i.e., (5000), as this means
nothing, and if the behaviour of the system changes, you can easily
change the sleep period in a single place in the code, rather than
grepping for 5000 !Dialog Box
"No", they cried, "What?", they gasped. Yes, sometimes you want to set some hardware up while you are in the middle of a test, for this I use a dialog. I've never checked in code to a repository that does this, every system I have worked on has had a fully automated test suite (i.e., no manual intervention). But, sometimes, for debugging it can be useful, i.e., pair an iphone before downloading an application requires someone to click on a dialog on the iOS device, therefore I need a dialog in the test code.
Hide Copy Code
DialogResult d = MessageBox.Show("Hello",
"Press the button when you want to run the app", MessageBoxButtons.OK);
Conclusion
Try and keep your tests atomic (i.e. test one thing), chaining functionality together can give results that always need investigating further. For example, a test installs an app on a phone. It fails. Ok this is single piece of functionality so investigate why.Another test attempts to install an app, run the app, close it, and then un-install the application. The test fails, ok now we have to debug to find out where it fails. If this test is testing the un-install, then this may be the only way to set the device up to test the un-install, so it's unavoidable. If this isn't the case, then split the test up into individual atomic parts and test them independently.
The desirable test results should indicate exactly what functionality is broken simply by which test fails.
I have caught 80% more bugs earlier on in the development lifecycle just from using a test-driven approach. It took me years to finally agree to doing it, and now I am using it I can't imagine developing in any other way. Take the plunge!
No comments:
Post a Comment