Then Dave Astels came along and pushed the envelope further with One Expectation per Example. This really grabbed my attention as I had been enjoying the benefits of keeping my test code clean with one assertion per test but anything with mocks in it just turned into out of control beasts (especially some of the more complex logic such as MVP). Even simple four liners such as below would end up with four expectations:
public AddOrder(OrderDto)Then everytime I needed to add new functionality I had to add more expectations and anyone who read the work (including myself after 24 hours) would struggle to make head or tale out of the monster. And if tests failed it would take a day to find which expectation went wrong. If you're not careful you end up with the TDD anti-pattern The Mockery.
{
Customer customer = session.GetCurrentCustomer();
customer.AddOrder(OrderFactory.CreateOrder(OrderDTO));
workspace.Save(customer);
}
I had read Dave Astels article several times but couldn't fathom out how it worked especially seeing it was written in Ruby with the behaviour driven RSpec. In the end I had to write it out into .NET myself before I got it.
So here is a break down of how I got Dave's One expectation per example to work for me:
One Expectation Per Example (C#)
One of the first things to note is Dave uses the builder pattern in his example. The idea is that the Address object interacts with a builder to pass it's data to rather than allowing objects to see it's state directly thus breaking encapsulation. I would like to go into this technique in more detail in another article but to deliver the point quickly think that you may create an HTML builder to display the address on the web.
Well let's start with Dave's first test:
[TestFixture]You may have noticed that I've changed a few things maily to make it look consistant with .NET design practices. Basically I've introduced a Parse method rather than the from_string method Dave uses.
public class OneExpectationPerExample
{
[Test]
public void ShouldCaptureStreetInformation()
{
Address addr = Address.Parse("ADDR1$CITY IL 60563");
Mockery mocks = new Mockery();
IBuilder builder = mocks.NewMock<IBuilder>();
Expect.Once.On(builder).SetProperty("Address1").To("ADDR1");
addr.Use(builder);
mocks.VerifyAllExpectationsHaveBeenMet();
}
}
Now we need to get this baby to compile. First we need to create the Address class like so:
public class AddressAnd the IBuilder interface:
{
public static Address Parse(string address)
{
throw new NotImplementedExpection();
}
public void Use(IBuilder builder)
{
throw new NotImplementedExpection();
}
}
public interface IBuilder {}Now it compiles but when we run it we get the following:
mock object builder does not have a setter for property Address1So we need to add the Address1 property to the IBuilder. Then we run and we get:
TestCase 'OneExpectationPerExample.ShouldCaptureStreetInformation'Let's implement some working code then:
failed: NMock2.Internal.ExpectationException : not all expected invocations were performed
Expected:
1 time: builder.Address1 = (equal to "ADDR1") [called 0 times]
public class AddressRun the tests again and they work! So let's move onto the second part of implementing the Csp. Here's the new test:
{
private readonly string address1;
private Address(string address1)
{
this.address1 = address1;
}
public static Address Parse(string address)
{
string[] splitAddress = address.Split('$');
return new Address(splitAddress[0]);
}
public void Use(IBuilder builder)
{
builder.Address1 = address1;
}
}
Now a little refactoring to get rid off our repeated code we turn it into this:
[Test]
public void ShouldCaptureCspInformation()
{
Address addr = Address.Parse("ADDR1$CITY IL 60563");
Mockery mocks = new Mockery();
IBuilder builder = mocks.NewMock<IBuilder%gt;();
Expect.Once.On(builder).SetProperty("Csp").To("CITY IL 60563");
addr.Use(builder);
mocks.VerifyAllExpectationsHaveBeenMet();
}
[TestFixture]Looking good! We run the new test and we get the usual error for having no Csp property on the IBuilder so we add that:
public class OneExpectationPerExample
{
private IBuilder builder;
private Address addr;
private Mockery mocks;
[SetUp]
public void SetUp()
{
mocks = new Mockery();
builder = mocks.NewMock<ibuilder>();
addr = Address.Parse("ADDR1$CITY IL 60563");
}
[TearDown]
public void TearDown()
{
mocks.VerifyAllExpectationsHaveBeenMet();
}
[Test]
public void ShouldCaptureStreetInformation()
{
Expect.Once.On(builder).SetProperty("Address1").To("ADDR1");
addr.Use(builder);
}
[Test]
public void ShouldCaptureCspInformation()
{
Expect.Once.On(builder).SetProperty("Csp").To("CITY IL 60563");
addr.Use(builder);
}
}
public interface IBuilderThen we run the test again and we get:
{
string Address1 { set; }
string Csp { set; }
}
TestCase 'OneExpectationPerExample.ShouldCaptureCspInformation'Oh no. This is where Dave's article falls apart for .NET.
failed: NMock2.Internal.ExpectationException : unexpected invocation of builder.Address1 = "ADDR1"
Expected:
1 time: builder.Csp = (equal to "CITY IL 60563") [called 0 times]
Basically RSpec has an option to create Quite Mocks which quitely ignore any unexpected calls. Unfortunately I know of no .NET mock libaries that have such behaviour (though I have since been reliably informed by John Donaldson on the tdd yahoo group that it is possible with the NUnit mock library) . Though there is a way out: stub the whole thing out by using Method(Is.Anything):
[Test]Just be careful to put the Stub AFTER the Expect and not before as NMock will use the Stub rather than the Expect and your test will keep failing.
public void ShouldCaptureCspInformation()
{
Expect.Once.On(builder).SetProperty("Csp").To("CITY IL 60563");
// stub it as we're not interested in any other calls.
Stub.On(builder).Method(Is.Anything);
addr.Use(builder);
}
So now we run the tests and we get:
TestCase 'OneExpectationPerExample.ShouldCaptureCspInformation'Excellent NMock is now behaving correctly we can finish implementing the code:
failed:
TearDown : System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation.
----> NMock2.Internal.ExpectationException : not all expected invocations were performed
Expected:
1 time: builder.Csp = (equal to "CITY IL 60563") [called 0 times]
public class AddressRun the test and it works! Now if we run the whole fixture we get:
{
private readonly string address1;
private readonly string csp;
private Address(string address1, string csp)
{
this.address1 = address1;
this.csp = csp;
}
public static Address Parse(string address)
{
string[] splitAddress = address.Split('$');
return new Address(splitAddress[0], splitAddress[1]);
}
public void Use(IBuilder builder)
{
builder.Address1 = address1;
builder.Csp = csp;
}
}
TestCase 'OneExpectationPerExample.ShouldCaptureStreetInformation'All we need to do do is go back and add the Stub code to the street test. That's a bit of a bummer but we could refactor our tests to do the call in the tear down like so:
failed: NMock2.Internal.ExpectationException : unexpected invocation of builder.Csp = "CITY IL 60563"
[TearDown]This approach comes across as slightly odd because the expectations are set in the test but the test is run in the tear down. I actually think this is neater in some ways as it ensures you have one test class for each set of behaviours the only off putting thing is the naming convention of the attributes.
public void UseTheBuilder()
{
Stub.On(builder).Method(Is.Anything);
addr.Use(builder);
mocks.VerifyAllExpectationsHaveBeenMet();
}
[Test]
public void ShouldCaptureStreetInformation()
{
Expect.Once.On(builder).SetProperty("Address1").To("ADDR1");
}
[Test]
public void ShouldCaptureCspInformation()
{
Expect.Once.On(builder).SetProperty("Csp").To("CITY IL 60563");
}
I won't bother continuing with the rest of Dave's article as it's just more of the same from here. The only thing I'd add is he does use one class per behaviour set (or context) so when he tests the behaviour of a string with ZIP code he uses a whole new test fixture. This can feel a little extreme in some cases as you get a bit of test class explosion but all in all it does make your life a lot easier.
I hope the translation helps all you .NET B/TDDers out there to free you from The Mockery.
Tip:
In more complex behaviours you may need to pass a value from one mock to another. In those instances you can do:
Stub.On(x).Method(Is.Anything).Will(Return.Value(y));
2 comments:
vfcj wgzdw porno makgpj e cg g uks
Post a Comment