Unit testing is an essential element in developing a maintainable, robust application. LightSpeed has been designed with testability in mind and provides several features designed specifically to facilitate unit testing a LightSpeed domain model.
Using a Real Database
A simple unit testing pattern (and the one we use internally) is to use transactional tests with a test database. To set this up do the following:
Setup a test database
Create an easily buildable test database (we suffix the name of ours with _Test). Apart from an indentical database schema, this database should contain a good set of test data used by the unit tests.
Create a base transactional test fixture
Transactional tests are run within a transaction so that any database updates are rolled back on completion of each test. To create transactional tests we simply create a base test fixture that our concrete unit test classes inherit from. The base fixture implements the transactional semantics through the SetUp and TearDown hook methods provided by our chosen unit test framework (NUnit, MBUnit etc.)
1[TestFixture] 2public abstract class TestFixtureBase 3{ 4 private static readonly LightSpeedContext _context = new LightSpeedContext(); 5 6 static TestFixtureBase() 7 { 8 // configure LightSpeed 9 _context.ConnectionString = @"Data Source=ORMapper;Initial Catalog=ORMapper;Integrated Security=SSPI"; 10 _context.Logger = new ConsoleLogger(); 11 } 12 13 private TransactionScope _transactionScope; 14 private IUnitOfWork _uow; 15 16 [SetUp] 17 public virtual void SetUp() 18 { 19 _transactionScope = new TransactionScope(); 20 _uow = _context.CreateUnitOfWork(); 21 } 22 23 [TearDown] 24 public virtual void TearDown() 25 { 26 _uow.Dispose(); 27 _transactionScope.Dispose(); 28 } 29}
Additionally, note that in the SetUp method we are creating a new unit of work and in the TearDown method we are destroying it by calling IUnitOfWork.Dispose. The creation of new units of work for each test is important because we want complete test isolation; that is, no test can affect the result of any other test. We achieve this isolation by having each test run in a new unit of work.
Create unit tests
Now we can go ahead and create some unit tests by deriving off our base test fixture. Here’s an example:
1[Test] 2public void UpdatingContributionDescription() 3{ 4 var contribution = _uow.FindById<Contribution>(1); 5 6 contribution.Description = "Some new description"; 7 8 _uow.SaveChanges(true); 9 10 var updatedContribution = _uow.FindById<Contribution>(1); 11 12 Assert.AreNotSame(contribution, updatedContribution); 13 Assert.AreEqual("Some new description", updatedContribution.Description); 14}
This test finds the Contribution with id 1 and then updates its Description attribute. We then call SaveChanges to flush our pending update to the database. Notice we pass true to the SaveChanges method. This is an idiomatic LightSpeed unit test pattern applicable whenever we are wanting to verify a database update was made correctly. The SaveChanges method causes all pending updates to be flushed to the database and the true argument signals that LightSpeed should additionally clear its internal Identity Map. The Identity Map is simply a cache of all entities that have been loaded within the current unit of work and serves to prevent us loading the same Entity more than once. By clearing the internal Identity Map, we guarantee that subsequent requests for previously loaded objects hit the database and therefore allow us to validate that the database state of an Entity is as we expect.
Using Fakes
LightSpeed includes two classes to help support faking queries and entities: TestUnitOfWork and EntityFactory. Both of these are in the Mindscape.LightSpeed.Testing namespace.TestUnitOfWork allows you to inject a fake result for a query. To do this, call the TestUnitOfWork.SetCollectionResult method, passing in the desired fake result. Subsequent calls to Find or FindBySql, or LINQ entity queries, will return the fake result rather than querying the database. TestUnitOfWork also allows faking of other query methods:
- Call TestUnitOfWork.SetSingleResult to specify a fake result for FindOne or FindById
- Call TestUnitOfWork.SetExpectedCountResult to specify a fake result for Count, or the LINQ Count() and LongCount() operators
- Call TestUnitOfWork.SetExpectedCalculateResult to specify a fake result for Calculate, or for LINQ aggregate operators such as Sum()
- Call TestUnitOfWork.SetProjectionResult to specify a fake result for Project, or for LINQ projection queries (where specific properties are selected into a named or anonymous type)
- Call TestUnitOfWork.SetSearchResult to specify a fake result for Search
Note that fake results are returned directly – any query expression or LINQ Where operator is not applied. Your test fixture should set up the expected fake results.
EntityFactory helps you to create entities with predictable ID values and in states other than the New state. Thus, you can use EntityFactory to fake entities that have been loaded from the database (and therefore begin in the Default state).
Using Mocks
If you are interested in interaction based unit testing (often characterized by the use of mocks, fakes or stubs), LightSpeed exposes a few key interfaces that facilitate such an approach. The primary extension point providing substitutability in LightSpeed is the IUnitOfWork interface. Implementing (or mocking) IUnitOfWork provides complete control over all major LightSpeed operations such as querying and persistence. In order to implement IUnitOfWork create an implementation of IUnitOfWorkFactory that returns your custom implementation and then assign your custom IUnitOfWorkFactory to the UnitOfWorkFactory property found on the LightSpeedContext object.