http://doi.ieeecomputersociety.org/10.1109/MS.2007.85
- First Law You may not write production code until you have written a failing unit test.
- Second Law You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
- Third Law You may not write more production code than is sufficient to pass the currently failing test.
From: Martin, Robert C.. Clean Code (Robert C. Martin Series) (p. 122). Pearson Education. Kindle Edition.
Let's see how we might apply these laws using nUnit and WPF.
Start a new Visual Studio project. As the first law states the test must exist before the production code, we will create an NUnit project. I'm using Visual Studio 2019, C#, and .Net Core. Call the project GetNowTest. Call the solution GetNow.
We will soon create the GetNow WPF project but before we can do that we need to write a test that fails. Not compiling is failure so change UnitTest1.cs to look like this.
using NUnit.Framework;
namespace GetNowTest
{
public class Tests
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsNotNull(mainWindow,
"Could not create MainWindow");
}
}
}
If you try to run this, you will get build errors. Yay - our test successfully failed!
Now let's add the project we're testing. Add a WPF App project with .Net Core, C# and call it GetNow. Make it the startup project.
Add a GetNow reference to the GetNowTest project.
You can build and run and the screen does nothing.
Right-click on the GetNowTest project and select Run Tests. The test fails. if you resize the Test Explorer window a bit you can see why.
You need to tell the test to run as an STA thread (you don't have to do this in .Net Framework). Change the test to look like this.
[Test][Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsNotNull(mainWindow,
"Could not create MainWindow");
}
Run the test again. It will pass.
The three rules state we create the test first so that it fails. Then we write the code. Then the test passes.
The next thing I want to write is the XAML. It will be a text block and a button. When the user clicks the button I will display the current date/time in the text block. There will also be a RoutedCommand. What can we test?
Well we could test that there is a textblock and a button but we're not supposed to be testing the view (that's the point of MVVM). We are supposed to test the ViewModel. As we're using MVVM we will have to implement INotifyPropertyChanged. I suppose we could test that.
Let's create a test that ensures MainWindow implements INotifyPropertyChanged. That will fail right now. Add a using for System.ComponentModel and change the test as shown below.
[Test][Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsTrue(mainWindow is INotifyPropertyChanged, "MainWindow does not implement INotifyPropertyChanged");
}
Run the tests and you will fail like this.
Alter the MainWindow code-behind so it looks like this.
using System.ComponentModel;
using System.Windows;
namespace GetNow
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
}
public event
PropertyChangedEventHandler PropertyChanged;
public void
PropChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
Now I would normally write the XAML but I can't think of a meaningful test for XAML. Perhaps there isn't one. As we're not supposed to be testing the View anyway, this all makes sense.
Let's write the XAML without writing a test for it.
<Window x:Class="GetNow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GetNow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<RoutedCommand x:Key="GetNowCommand"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{StaticResource GetNowCommand}" Executed="GetNow_Executed"/>
</Window.CommandBindings>
<StackPanel Orientation="Horizontal" Height="20">
<TextBlock Width="200" Text="{Binding Now}"/>
<Button Content="Get
Now" Command="{StaticResource GetNowCommand}"/>
</StackPanel>
</Window>
We will need to define a stub for GetNow_Executed in MainWindow.xaml.cs.
. private void GetNow_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
}
Our next test is to define our Now property but before we do that we need to create a failing test. Let's add an assertion the Now property is set to MinValue when the class is first instantiated. Because it does not exist - this will cause a build error which counts as a failure.
[Test]
[Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsTrue(mainWindow is INotifyPropertyChanged, "MainWindow does not implement INotifyPropertyChanged");
}
Now our test has failed, we can add the Now property to the code-behind.
private DateTime _Now;
public DateTime Now
{
get { return _Now; }
set
{
_Now = value;
PropChanged("Now");
}
}
private DateTime _Now = DateTime.Now;
The last thing to do is complete the GetNow_Executed stub. There should be no substantial code in event handlers (how many times have you put a ton of functionality in an event handler, then had to refactor it out so it can be called from somewhere else too). We will create a function called SetNow. but before we do this, create a failing test.
[Test]
[Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsNotNull(mainWindow,
"Could not create MainWindow");
Assert.IsTrue(mainWindow is INotifyPropertyChanged, "MainWindow does not implement INotifyPropertyChanged");
mainWindow.SetNow();
Assert.AreNotEqual(expected:
DateTime.MinValue, actual:mainWindow.Now, "Now
was not updated by GetNowCommand routed command");
}
This test will not compile so it fails correctly.
Now replace the stub with this.
private void
GetNow_Executed(object sender,
System.Windows.Input.ExecutedRoutedEventArgs e)
{
SetNow();
}
public void SetNow()
{
Now = DateTime.Now;
}
If you run this and click the button the current date and time is displayed.
If you run the tests they all pass.
Now let's suppose Richard comes along next year. He breaks your code by commenting out the contents of SetNow like this.
public void SetNow()
{
//Now =
DateTime.Now;
}
He realizes his mistake and comments out a different line instead.
private void
GetNow_Executed(object sender,
System.Windows.Input.ExecutedRoutedEventArgs e)
{
//SetNow();
}
public void SetNow()
{
Now = DateTime.Now;
}
The new code passes all the test but has broken the application. The date/time is not updated when the user clicks the button. Does this mean the tests don't work? No, it means they are incomplete. We could replace the last test with one that simulates a button press. We could do this by simply executing GetNow_Executed directly (we will need to change the scope to public) or by getting a reference to the RoutedCommand and calling executed on it (which starts to drift into testing the View).
Option 1 - execute the event handler directly.
[Test]
[Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsNotNull(mainWindow,
"Could not create MainWindow");
Assert.IsTrue(mainWindow is INotifyPropertyChanged, "MainWindow does not implement INotifyPropertyChanged");
mainWindow.GetNow_Executed(mainWindow, null);
Option 2 - execute via the RoutedCommand which ensures the routed command is bound correctly.
[Test]
[Apartment(System.Threading.ApartmentState.STA)]
public void Test1()
{
GetNow.MainWindow mainWindow = new GetNow.MainWindow();
Assert.IsTrue(mainWindow is INotifyPropertyChanged, "MainWindow does not implement INotifyPropertyChanged");
RoutedCommand GetNow =
(RoutedCommand)mainWindow.TryFindResource("GetNowCommand");
Assert.IsNotNull(GetNow);
GetNow.Execute(null, mainWindow);
}
How many tests should be in a single test method? Some people say there should only be one Assert. Others say a test should cover a "concept". For example, adding an element to a collection can fail in several ways, each of which requires an assert. It is a single concept. Adding and deleting elements from a collection are two different concepts that deserve separate unit tests.
This gives rise to a common problem. It is tempting to combine tests with expensive setups. Perhaps you need to fetch some valid objects from your database. You might be able to MOQ this, but sometimes not. Perhaps some just-in-time routines to share expensive resources might help.
In my opinion, whenever a bug is found in a use case that has no current test, we have to write a new test that fails before the code is fixed and passes once it is fixed. Therefore it is critical to know what the use case for each test is.
No comments:
Post a Comment