In looking at some stack overflow threads it doesn't seem like interfaces must strictly behave as reference types, but they will in NSubstitute's Substitute.For implementations and that opens up some interesting approaches for testing long (or longish) workflows wherein there exists a series of hurdles to be cleared. Consider this controller...
using System.Net.Http;
using System.Web.Http;
using Hurdles.Core.Interfaces;
using Hurdles.Core.Objects;
namespace Hurdles.UserInterface.Controllers
{
public class PersonController : ApiController
{
private IPersonRepository _personRepository;
public PersonController(IPersonRepository personRepository)
{
_personRepository = personRepository;
}
public HttpResponseMessage Post(Person person)
{
if (person == null)
{
return Request.CreateResponse(HttpStatusCode.NotImplemented, "Person not given.");
}
if (person.SocialSecurityNumber == null)
{
return Request.CreateResponse(HttpStatusCode.NotImplemented, "SSN not given.");
}
if (_personRepository.IsExisting(person))
{
return Request.CreateResponse(HttpStatusCode.NotImplemented, "SSN preexists.");
}
if (_personRepository.IsCriminal(person))
{
return Request.CreateResponse(HttpStatusCode.NotImplemented, "Criminal!");
}
person = _personRepository.SavePerson(person);
return Request.CreateResponse(HttpStatusCode.Created, person);
}
}
}
Again, there are a few hurdles to be cleared here in the name of a happy pass success and two of them revolve around the external dependency on IPersonRepository which we will mock in test. For a happy pass test to succeed in which a person is created, a person must first pass a sanity check to ensure that it does not have a preexisting social security number and must then moreover pass a sanity check to ensure that the individual is not "looked up" to found to be a criminal. (It's a goofy example. I admit it. Whatever.) All of the tests below pass and they are listed sequentially in the order of a process which validates each step in the controller's workflow sequentially. Our workflow is really little more than a straight path with some hurdles set up upon it like so:
The thing to observe in the tests is how interface method signatures may be mocked only when the time finally comes to mock them, reconfigured when needed, and then left to be in future tests. For example, setting .IsExisting to return false needs to only to be done once in the second to last test even though .IsExisting must return false for both the last and second to last test. Once such a setting is made, the setting is stuck until reset. All the modifications befall the same pointer which is reassigned to a new controller at the beginning of each test. You can probably imagine how this might keep code clean in a sequence three or four times as long as the example here.
using System.Web.Http;
using Hurdles.Core.Interfaces;
using Hurdles.Core.Objects;
using Hurdles.UserInterface.Controllers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
namespace Hurdles.UserInterface.Tests.Controllers
{
[TestClass]
public class PersonControllerTests
{
private PersonController _director;
private IPersonRepository _prop = Substitute.For<IPersonRepository>();
private string _scene;
[TestInitialize()]
public void SetupTest()
{
_director = new PersonController(_prop);
}
[TestMethod]
public void lack_of_person_causes_failure()
{
_director.Request = new HttpRequestMessage();
_director.Configuration = new HttpConfiguration();
HttpResponseMessage httpResponseMessage = _director.Post(null);
httpResponseMessage.TryGetContentValue<string>(out _scene);
Assert.AreEqual(_scene, "Person not given.");
}
[TestMethod]
public void lack_of_social_security_number_causes_failure()
{
Person actor = new Person();
_director.Request = new HttpRequestMessage();
_director.Configuration = new HttpConfiguration();
HttpResponseMessage httpResponseMessage = _director.Post(actor);
httpResponseMessage.TryGetContentValue<string>(out _scene);
Assert.AreEqual(_scene, "SSN not given.");
}
[TestMethod]
public void preexisting_social_security_number_causes_failure()
{
Person actor = new Person() { SocialSecurityNumber = 457555462 };
_prop.IsExisting(Arg.Any<Person>()).Returns(true);
_director.Request = new HttpRequestMessage();
_director.Configuration = new HttpConfiguration();
HttpResponseMessage httpResponseMessage = _director.Post(actor);
httpResponseMessage.TryGetContentValue<string>(out _scene);
Assert.AreEqual(_scene, "SSN preexists.");
}
[TestMethod]
public void preexisting_criminal_history_causes_failure()
{
Person actor = new Person() { SocialSecurityNumber = 457555462 };
_prop.IsExisting(Arg.Any<Person>()).Returns(false);
_prop.IsCriminal(Arg.Any<Person>()).Returns(true);
_director.Request = new HttpRequestMessage();
_director.Configuration = new HttpConfiguration();
HttpResponseMessage httpResponseMessage = _director.Post(actor);
httpResponseMessage.TryGetContentValue<string>(out _scene);
Assert.AreEqual(_scene, "Criminal!");
}
[TestMethod]
public void happy_pass()
{
Person actor = new Person() { SocialSecurityNumber = 457555462 };
_prop.IsCriminal(Arg.Any<Person>()).Returns(false);
_prop.SavePerson(Arg.Any<Person>()).Returns(new Person() {Id = 1} );
_director.Request = new HttpRequestMessage();
_director.Configuration = new HttpConfiguration();
HttpResponseMessage httpResponseMessage = _director.Post(actor);
httpResponseMessage.TryGetContentValue<Person>(out actor);
Assert.AreEqual(actor.Id, 1);
}
}
}
No comments:
Post a Comment