Testing is an important part of a developers responsibilities, especially in a fast moving agile world! This is the first post in a series on testing patterns I used and discovered when testing applications using XAF and XPO applications.
Although the patterns I use are focusing on testing XAF and XPO applications, most of them may apply to not only on testing and not only on XAF applications. After learning about the builder pattern in the last post we now can focus on providing test data for our tests. So let's get started!
Test data
Before we talk about testing business logic, we need to think about test data first. What data is expected, which is unexpected. What happens if we for example have an external system putting data into our database, bypassing application logic and validation.
Providing test data especially for lookup data is a tedious task, but now we know the bloch's builder pattern we can use it to provide test data, and seed our data.
In this example I will use a simple time tracking app. An employee can record an time entry on a project with an specific activity.
[Persistent("Person")]
public class Person : MakeifyBaseObject
{
public Person(Session session) : base(session) { }
string _Salutation;
[Persistent("Salutation")]
public string Salutation { get => _Salutation; set => SetPropertyValue(ref _Salutation, value); }
string _FirstName;
[Persistent("FirstName")]
public string FirstName { get => _FirstName; set => SetPropertyValue(ref _FirstName, value); }
string _LastName;
[Persistent("LastName")]
public string LastName { get => _LastName; set => SetPropertyValue(ref _LastName, value); }
}
[Persistent("Project")]
public class Project : MakeifyBaseObject
{
public Project(Session session) : base(session) { }
string _Name;
[Persistent("Name")]
public string Name { get => _Name; set => SetPropertyValue(ref _Name, value); }
}
[Persistent("Activity")]
public class Activity : MakeifyBaseObject
{
public Activity(Session session) : base(session) { }
string _Name;
[Persistent("Name")]
public string Name { get => _Name; set => SetPropertyValue(ref _Name, value); }
}
[Persistent("TimeEntry")]
public class TimeEntry : MakeifyBaseObject
{
public TimeEntry(Session session) : base(session) { }
Activity _Activity;
[Persistent("Activity")]
public Activity Activity { get => _Activity; set => SetPropertyValue(ref _Activity, value); }
Project _Project;
[Persistent("Project")]
public Project Project { get => _Project; set => SetPropertyValue(ref _Project, value); }
Person _Employee;
[Persistent("Employee")]
public Person Employee { get => _Employee; set => SetPropertyValue(ref _Employee, value); }
DateTime? _Start;
[Persistent("Start")]
public DateTime? Start { get => _Start; set => SetPropertyValue(ref _Start, value); }
DateTime? _End;
[Persistent("End")]
public DateTime? End { get => _End; set => SetPropertyValue(ref _End, value); }
string _Description;
[Persistent("Description")]
[Size(SizeAttribute.Unlimited)]
public string Description { get => _Description; set => SetPropertyValue(ref _Description, value); }
}
As you can see there is nothing fancy, just a bunch of persistent classes. Let's think about a possible business requirement:
- As a manager I want to know the project times, so I can charge the customer money.
- Sum up all time entries grouped by project, employee and activity.
- If there is no project it goes on the no project pile
- if there is no activity it goes on the general activity pile
- If there is no employee it goes on the no employee pile
- If there is no start date it will count as 0
- If there is no end date it count as 0
So we know for sure, we will need test data for projects, activities and employees. If we want to test only the business logic, we can write a unit test, so just use an XPObjectSpaceProviderBuilder
, setup a TestFixture
and use custom builders to prepare the test case. For now we will just do it as an InMemory
test, but later on, we can do an integration test, with a real database to be sure we get the expected results (for example in a nightly build on the CI-Server).
Let's look real quick at the PersonBuilder
cause it contains a new concept for the builder pattern called Guards
.
public abstract class XpObjectBuilder<TBuilder, TObject>
where TObject : IXPObject
where TBuilder : XpObjectBuilder<TBuilder, TObject>
{
protected TBuilder This => (TBuilder)this;
Session _Session;
protected Session Session
{
get
{
GuardSession();
return _Session;
}
set => _Session = value;
}
protected virtual void GuardSession()
{
if(_Session == null)
{
throw new ArgumentNullException(nameof(Session), $"Cannot create object without '{nameof(Session)}' use '{nameof(WithSession)}' to create '{typeof(TObject).FullName}'");
}
}
public TBuilder WithSession(Session session)
{
Session = session;
return This;
}
}
public class PersonBuilder : PersonBuilder<PersonBuilder, Person> { };
public class PersonBuilder<TBuilder, TPerson> : XpObjectBuilder<TBuilder, TPerson>
where TPerson : Person
where TBuilder : PersonBuilder<TBuilder, TPerson>
{
protected virtual TPerson Create() => (TPerson)new Person(Session);
public virtual TPerson Build()
{
var person = Create();
person.FirstName = FirstName ?? person.FirstName;
person.Salutation = Salutation ?? person.Salutation;
person.LastName = LastName ?? person.LastName;
return person;
}
protected string Salutation { get; set; }
public TBuilder WithSalutation(string salutation)
{
Salutation = salutation;
return This;
}
protected string FirstName { get; set; }
public TBuilder WithFirstName(string firstName)
{
FirstName = firstName;
return This;
}
protected string LastName { get; set; }
public TBuilder WithLastName(string lastName)
{
LastName = lastName;
return This;
}
}
Cause the person constructor needs an Session
instance, we guard from improper usage, and inform the developer using the builder to use WithSession
first.
Of course we could use a constructor to provide a Session
(for example with DependencyInjection), but for now let's keep it like this. After that we can create a JohnDoePersonBuilder:
public class JohnDoePersonBuilder : JohnDoePersonBuilder<JohnDoePersonBuilder, Person> { };
public class JohnDoePersonBuilder<TBuilder, TPerson> : PersonBuilder<TBuilder, TPerson>
where TPerson : Person
where TBuilder : JohnDoePersonBuilder<TBuilder, TPerson>
{
public JohnDoePersonBuilder()
{
WithSalutation("Mr.");
WithFirstName("John");
WithLastName("Doe");
}
}
Now we can use the JohnDoePersonBuilder
in our tests or even in production (for example when seeding the database). This allows the team to agree on a set of test data, but is able to change some properties in test variations.
Test structure & Lazy test fixtures
After defining data we need in our tests, we can talk about another pattern that comes in handy when dealing with expensive test resources. Lazy
It makes sure the instance you want to produces lazily, is created at the first usage, afterwards it always returns the same instance. Feels like a singleton, but of course it's not. Singletons are evil (esp. when it comes to testing)!
We are using xUnit here, but the following pattern will work in NUnit as well, the usage is a little bit different.
Test fixtures
Test fixtures allow us to share resources across tests. That's neat, normally we shouldn't do that, cause tests should be independent. But if resources are expensive to create, they are the way to go. Faster tests are better than slower tests, and slow tests means slow feedback. Slow feedback leads to no automatic testing, so let's not do that.
public class TimeEntryFixture : IDisposable, IObjectSpaceFactory
{
public TimeEntryFixture()
{
var typesInfo = new TypesInfo();
var typesInfoSource = new XpoTypeInfoSourceBuilder()
.WithTypesInfo(typesInfo)
.WithTypes(ModelTypes.Types.ToArray()) // Types that the test uses
.Build();
ObjectSpaceProviderBuilder = new XPObjectSpaceProviderBuilder()
.InMemory() // Here comes the power of builders
.WithTypesInfo(typesInfo)
.WithTypesInfoSource(typesInfoSource);
// Here is the lazy magic
_ObjectSpaceProvider = new Lazy<IObjectSpaceProvider>(() => ObjectSpaceProviderBuilder.Build());
}
public XPObjectSpaceProviderBuilder ObjectSpaceProviderBuilder { get; }
// The Lazy instance
private Lazy<IObjectSpaceProvider> _ObjectSpaceProvider;
// Once Value is accessed for the first time, the ObjectSpaceProvider is created an will be used until the process stops
public IObjectSpaceProvider ObjectSpaceProvider => _ObjectSpaceProvider.Value;
public void Dispose()
{
// Only dispose if the actual value was created
// -> If nothing was created, there is nothing we need to dispose
if(_ObjectSpaceProvider.IsValueCreated && _ObjectSpaceProvider.Value is IDisposable)
{
((IDisposable)ObjectSpaceProvider).Dispose();
}
}
// ObjectSpaceFactory will be covered later in this series
public IObjectSpace CreateObjectSpace(Type objectType) => ObjectSpaceProvider.CreateObjectSpace();
// Drop the database.
public TimeEntryFixture ClearDataBase()
{
using(var os = ObjectSpaceProvider.CreateObjectSpace())
{
((XPObjectSpace)os).Session.ClearDatabase();
}
// Just for cosmetics, then we can use expression body constructors in our tests
return this;
}
}
The actual test
Now let's look at the usage:
// Extension method to provide a session to the builder
// There is a more elegant way, but we cover that later
public static class XpObjectBuilderExtensions
{
public static TBuilder WithObjectSpace<TBuilder, TObject>(this TBuilder builder, IObjectSpace objectSpace)
where TObject : IXPObject
where TBuilder : XpObjectBuilder<TBuilder, TObject>
=> builder.WithSession(((XPObjectSpace)objectSpace).Session);
}
// We are using the TimeEntryFixture
public class TimeEntryTests : IClassFixture<TimeEntryFixture>
{
readonly TimeEntryFixture _Fixture;
public TimeEntryTests(TimeEntryFixture fixture)
// Clear the database every time a test is executed
=> _Fixture = fixture.ClearDataBase();
[Fact]
public void JohnDoeShouldExist()
{
// Arrange: Create test data
using(var os = _Fixture.ObjectSpaceProvider.CreateObjectSpace())
{
var johnDoe = new JohnDoePersonBuilder()
// This is a little bit ugly now, but we will cover that in a later post.
.WithObjectSpace<JohnDoePersonBuilder, Person>(os)
.Build();
os.CommitChanges();
}
using(var os = _Fixture.ObjectSpaceProvider.CreateObjectSpace())
{
//Act: find the person created
var person = os.FindObject<Person>(null);
//Assert
person.ShouldSatisfyAllConditions(
() => os.GetObjectsCount(typeof(Person), null).ShouldBe(1),
() => person.ShouldNotBeNull(),
() => person.Salutation.ShouldBe("Mr."),
() => person.FirstName.ShouldBe("John"),
() => person.LastName.ShouldBe("Doe")
);
}
}
[Fact]
public void JaneDowShouldExist()
{
//Arrange: Now test data is slightly modified
using(var os = _Fixture.ObjectSpaceProvider.CreateObjectSpace())
{
var johnDoe = new JohnDoePersonBuilder()
.WithObjectSpace<JohnDoePersonBuilder, Person>(os)
.WithSalutation("Mrs.")
.WithFirstName("Jane")
.Build();
os.CommitChanges();
}
using(var os = _Fixture.ObjectSpaceProvider.CreateObjectSpace())
{
//Act: Find the person created
var person = os.FindObject<Person>(null);
//Assert: We didn't specify Doe, but of course it's still there
person.ShouldSatisfyAllConditions(
() => person.ShouldNotBeNull(),
() => person.Salutation.ShouldBe("Mrs."),
() => person.FirstName.ShouldBe("Jane"),
() => person.LastName.ShouldBe("Doe")
);
}
}
[Fact]
public void JohnDoeShouldNotExist()
{
//Cause the database is cleared each time, nothing is there.
using(var os = _Fixture.ObjectSpaceProvider.CreateObjectSpace())
{
var person = os.FindObject<Person>(null);
person.ShouldBeNull();
}
}
}
Based on the comment's I made in the code, can you spot the places that probably need refactoring? Think about it and let me know in the comments! And what do you think is IObjectSpaceFactory
is about?
Recap
We learned the basics of not duplicating test data, unit testing when we need data access using an ObjectSpaceProvider
and now we are prepared for the next post how to test and structure business logic.
Test code is equally important production code! Treat it with care! Refactor it like you would do production code!
If you find interesting what I'm doing, consider becoming a patreon or contact me for training, development or consultancy.
I hope this pattern helps testing your applications. The next post in this series will cover testing business logic. The source code the XpoTypeInfoSourceBuilder and XPObjectSpaceProviderBuilder is on github. The code of the actual tests will be online later this week.
Happy testing!
Comments
Andre S. 3 Jul 2019 10:07
Great article! Thank you! I'm looking forward to part 3 of the series. I really like the ObjectSpaceProviderBuilder and the XpoTypeInfoSourceBuilder. But I'm not sure, if I like the XpObjectBuilder. Of course the fluent style of
looks quite good. But it necessary to write a lot of stupid code for the builders. A simple static helper like
will also work. Because we should use public setters for the properties in XAF/XPO, the BuilderPattern does not bring much benefit. Am I missing something?
Thank you
Your comment will appear in a few minutes.
Manuel Grundner 3 Jul 2019 10:46
Hi Andre!
The main idea about the pattern is: you can change some aspects of the object in a specific test an be sure that no side effects occur.
In deed a simple helper is often enough, but as time fly's by and applications get larger and more complicated, so does test code. That's a code smell many teams miss.
Another example is for example: Take an entity Email, there is an incomming one and an outgoing one, both have different semantics (but share some properties) but are stored in the same table. With 2 separate builders you can eliminate miss usage.
Back to the testing example:
Imagine a project object. Each project has a kind property and a staff list containing the people working on it. Each staff can have a different role (manager and worker):
If the team agrees about the semantics, everybody knows what to expect and what defaults are set. Now you can take a shorthand and derive an DefaultProjectBuilder.
But if you use it and want to set a different kind for example:
No ConstructionKind is ever generated! So tests are focused and fast.
Hope this clears.
Ps.: Yeah it's a lot of code, but with some snipplet's or even code generators it's not that painful to maintain. Pps: Builders should be focused on the domain problem not on the orm or data layer.
Thank you
Your comment will appear in a few minutes.
Andre S. 3 Jul 2019 11:31
Does make sense. With my helper/extension method approach a ConstructionKind (and also all other references in the object graph of the ConstructionKind) would be created and later be replaced with the MedicalKind. Somehow ugly. But do you really expect to see a performance impact?
Using t4 templates for the builders sounds like a nice approach.
Thank you
Your comment will appear in a few minutes.
Manuel Grundner 3 Jul 2019 11:52
It's not only about performance, but rather having a clean dataset in memory. Less side effects for tests. Performance wise: sure not with 100 tests, but if you've got 10000k or more sure! Sums up really quick with datadriven tests.
Cause I'm hard into dotnet core and msbuild right now (there is as far I know no T4) maybe a msbuild based version would be possible. Another option would be a roslyn analyzer with code fix!
But to be honest, I don't know it it would be worth the effort, cause builders can be rather complicated. But for a getting started it would be a huge timesaver.
Thank you
Your comment will appear in a few minutes.
Andre S. 3 Jul 2019 12:07
Sorry to bother you, but could you explain this in more detail? Of course there are additional/orphaned records in the dataset. But the application shouldn't care. What side effects are possible?
I think I also should start to work with dotnet core. I found t5 project for dotnet core. Don't know if it works...
Thank you
Your comment will appear in a few minutes.
Manuel Grundner 3 Jul 2019 12:30
No worries. If you want to hear the meaning of an consultant: It depends. But let me explain further: An example I encountered not all to long ago:
Project has a Kind Reference. They wanted to somehow group them together:
There are 3 kinds of project kinds (oh what a naming :D)
The task was to build a statistical model about their data grouped by Floor, Root and Misc kind. If its Construction it should be associated with the misc kind (cause they only wanted to know what's on the specific to calucate some costs). With an naive implementation (written by a junior without the big picture in mind) it could look like:
But in the default "testhelper" there where defined the default groups: Construction-Floors and Construction-Roofs (it was in reality not that easy, but for simplicity i will stick with this now)
So the test code was like:
So there where some data in the test database that nobody expected. The code written was totally correct, the test was lying. Cause there was data nobody expected.
Of course this is a really special problem, it puzzeled 2 seniors, 1 junior (which had no idea what was wrong) and 1 consultant to isolate the problem.
But the main point is: They lost trust in tests and thats the MAIN problem. You never should loose trust in your tests. Isolation is key to keep trust in tests. (software development is hard enough. Getting a testcase in your head should be as easy as possible)
Hope that answeres your question.
Thank you
Your comment will appear in a few minutes.
Thank you
Your comment will appear in a few minutes.