Skip to content

Commit 6f3053f

Browse files
committed
Documentation update
1 parent e0d928f commit 6f3053f

File tree

2 files changed

+175
-113
lines changed

2 files changed

+175
-113
lines changed

README.md

Lines changed: 167 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -12,76 +12,79 @@ Prior to v2.0 this library was known as NTestDataBuilder.
1212

1313
1. `Install-Package TestStack.Dossier`
1414

15-
2. Create a builder class for one of your objects, e.g. if you have a customer:
16-
17-
// Customer.cs
18-
19-
public class Customer
20-
{
21-
protected Customer() {}
22-
23-
public Customer(string firstName, string lastName, int yearJoined)
24-
{
25-
if (string.IsNullOrEmpty(firstName))
26-
throw new ArgumentNullException("firstName");
27-
if (string.IsNullOrEmpty(lastName))
28-
throw new ArgumentNullException("lastName");
29-
30-
FirstName = firstName;
31-
LastName = lastName;
32-
YearJoined = yearJoined;
33-
}
34-
35-
public virtual int CustomerForHowManyYears(DateTime since)
36-
{
37-
if (since.Year < YearJoined)
38-
throw new ArgumentException("Date must be on year or after year that customer joined.", "since");
39-
return since.Year - YearJoined;
40-
}
41-
42-
public virtual string FirstName { get; private set; }
43-
public virtual string LastName { get; private set; }
44-
public virtual int YearJoined { get; private set; }
45-
}
46-
47-
// CustomerBuilder.cs
48-
49-
public class CustomerBuilder : TestDataBuilder<Customer, CustomerBuilder>
50-
{
51-
public CustomerBuilder()
52-
{
53-
// Can set up defaults here - any that you don't set will have an anonymous value generated by default.
54-
WhoJoinedIn(2013);
55-
}
56-
57-
public CustomerBuilder WithFirstName(string firstName)
58-
{
59-
return Set(x => x.FirstName, firstName);
60-
}
61-
62-
public CustomerBuilder WithLastName(string lastName)
63-
{
64-
return Set(x => x.LastName, lastName);
65-
}
66-
67-
public CustomerBuilder WhoJoinedIn(int yearJoined)
68-
{
69-
return Set(x => x.YearJoined, yearJoined);
70-
}
71-
72-
protected override Customer BuildObject()
73-
{
74-
return new Customer(
75-
Get(x => x.FirstName),
76-
Get(x => x.LastName),
77-
Get(x => x.YearJoined)
78-
);
79-
}
80-
}
15+
2. Create a builder class for one of your domain objects, e.g. if you have a customer:
16+
17+
// Customer.cs
18+
19+
public class Customer
20+
{
21+
protected Customer() {}
22+
23+
public Customer(string firstName, string lastName, int yearJoined)
24+
{
25+
if (string.IsNullOrEmpty(firstName))
26+
throw new ArgumentNullException("firstName");
27+
if (string.IsNullOrEmpty(lastName))
28+
throw new ArgumentNullException("lastName");
29+
30+
FirstName = firstName;
31+
LastName = lastName;
32+
YearJoined = yearJoined;
33+
}
34+
35+
public virtual int CustomerForHowManyYears(DateTime since)
36+
{
37+
if (since.Year < YearJoined)
38+
throw new ArgumentException("Date must be on year or after year that customer joined.", "since");
39+
return since.Year - YearJoined;
40+
}
41+
42+
public virtual string FirstName { get; private set; }
43+
public virtual string LastName { get; private set; }
44+
public virtual int YearJoined { get; private set; }
45+
}
46+
47+
// CustomerBuilder.cs
48+
49+
public class CustomerBuilder : TestDataBuilder<Customer, CustomerBuilder>
50+
{
51+
public CustomerBuilder()
52+
{
53+
// Can set up defaults here - any that you don't set or subsequently override will have an anonymous value generated by default.
54+
WhoJoinedIn(2013);
55+
}
56+
57+
// Note: the methods are virtual - this is important if you want to build lists (as per below)
58+
public virtual CustomerBuilder WithFirstName(string firstName)
59+
{
60+
return Set(x => x.FirstName, firstName);
61+
}
62+
63+
public virtual CustomerBuilder WithLastName(string lastName)
64+
{
65+
return Set(x => x.LastName, lastName);
66+
}
67+
68+
public virtual CustomerBuilder WhoJoinedIn(int yearJoined)
69+
{
70+
return Set(x => x.YearJoined, yearJoined);
71+
}
72+
73+
protected override Customer BuildObject()
74+
{
75+
return new Customer(
76+
Get(x => x.FirstName),
77+
Get(x => x.LastName),
78+
Get(x => x.YearJoined)
79+
);
80+
// or
81+
return BuildUsing<CallConstructorFactory>();
82+
}
83+
}
8184

8285
3. Use the builder in a test, e.g.
8386

84-
var customer = new CustomerBuilder().WithFirstName("Robert").Build();
87+
var customer = new CustomerBuilder().WithFirstName("Robert").Build();
8588

8689
4. Consider using the Object Mother pattern in combination with the builders, see [my blog post](http://robdmoore.id.au/blog/2013/05/26/test-data-generation-the-right-way-object-mother-test-data-builders-nsubstitute-nbuilder/) for a description of how I use this library.
8790

@@ -92,75 +95,134 @@ This library allows you to build a list of entities fluently and tersely. Here i
9295

9396
```c#
9497
var customers = CustomerBuilder.CreateListOfSize(5)
95-
.TheFirst(1).WithFirstName("First")
96-
.TheNext(1).WithLastName("Next Last")
97-
.TheLast(1).WithLastName("Last Last")
98-
.ThePrevious(2).With(b => b.WithLastName("last" + (++i).ToString()))
99-
.All().WhoJoinedIn(1999)
100-
.BuildList();
98+
.TheFirst(1).WithFirstName("First")
99+
.TheNext(1).WithLastName("Next Last")
100+
.TheLast(1).WithLastName("Last Last")
101+
.ThePrevious(2).With(b => b.WithLastName("last" + (++i).ToString()))
102+
.All().WhoJoinedIn(1999)
103+
.BuildList();
101104
```
102105

103106
This would create the following (represented as json):
104107

105108
```json
106109
[
107-
{
108-
"FirstName":"First",
109-
"LastName":"LastNameff51d5e5-9ce4-4710-830e-9042cfd48a8b",
110-
"YearJoined":1999
111-
},
112-
{
113-
"FirstName":"FirstName7b08da9c-8c13-47f7-abe9-09b73b935e1f",
114-
"LastName":"Next Last",
115-
"YearJoined":1999
116-
},
117-
{
118-
"FirstName":"FirstName836d4c54-b227-4c1b-b684-de4cd940c251",
119-
"LastName":"last1",
120-
"YearJoined":1999
121-
},
122-
{
123-
"FirstName":"FirstName5f53e895-921e-4130-8ed8-610b017f3b9b",
124-
"LastName":"last2",
125-
"YearJoined":1999
126-
},
127-
{
128-
"FirstName":"FirstName9cf6b05f-38aa-47c1-9fd7-e3c1009cf3e4",
129-
"LastName":"Last Last",
130-
"YearJoined":1999
131-
}
110+
{
111+
"FirstName":"First",
112+
"LastName":"LastNameff51d5e5-9ce4-4710-830e-9042cfd48a8b",
113+
"YearJoined":1999
114+
},
115+
{
116+
"FirstName":"FirstName7b08da9c-8c13-47f7-abe9-09b73b935e1f",
117+
"LastName":"Next Last",
118+
"YearJoined":1999
119+
},
120+
{
121+
"FirstName":"FirstName836d4c54-b227-4c1b-b684-de4cd940c251",
122+
"LastName":"last1",
123+
"YearJoined":1999
124+
},
125+
{
126+
"FirstName":"FirstName5f53e895-921e-4130-8ed8-610b017f3b9b",
127+
"LastName":"last2",
128+
"YearJoined":1999
129+
},
130+
{
131+
"FirstName":"FirstName9cf6b05f-38aa-47c1-9fd7-e3c1009cf3e4",
132+
"LastName":"Last Last",
133+
"YearJoined":1999
134+
}
132135
]
133136
```
137+
134138
### Castle Dynamic Proxy Generator Exception error
135139

136140
If you use the list builder functionality and get the following error:
137141

138-
> Castle.DynamicProxy.Generators.GeneratorExceptionCan not create proxy for type <YOUR_BUILDER_CLASS> because it is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] attribute, because assembly <YOUR_TEST_ASSEMBLY> is not strong-named.
142+
> Castle.DynamicProxy.Generators.GeneratorException: Can not create proxy for type <YOUR_BUILDER_CLASS> because it is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] attribute, because assembly <YOUR_TEST_ASSEMBLY> is not strong-named.
139143
140144
Then you either need to:
141145

142146
* Make your builder class public
143147
* Add the following to your `AssemblyInfo.cs` file: `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]`
144148

149+
### Non-virtual method Invalid Operation Exception
150+
151+
If you use the list builder functionality and get the following error:
152+
153+
> System.InvalidOperationException: Tried to build a list with a builder who has non-virtual method. Please make <METHOD_NAME> on type <YOUR_BUILDER_CLASS> virtual.
154+
155+
Then you need to mark all the public methods on your builder as virtual. This is because we are using Castle Dynamic Proxy to generate lists and it can't intercept non-virtual methods.
156+
145157
Create Entities Implicitly
146158
--------------------------
147-
In the previous examples, you have seen how to create entities *explicitly*, by calling the `Build()` and `BuildList()` methods. For the ultimate in terseness, you can omit these methods, and Dossier will *implicitly* call them for you. The one caveat is that you must explicitly declare the variable type rather than using the `var` keyword.
159+
In the previous examples, you have seen how to create entities *explicitly*, by calling the `Build()` and `BuildList()` methods. For the ultimate in terseness, you can omit these methods, and Dossier will *implicitly* call them for you. The one caveat is that you must explicitly declare the variable type rather than using the `var` keyword (unless you are passing into a method with the desired type).
148160

149161
So, to create a single entity:
150162

151-
Customer customer = new CustomerBuilder();
163+
Customer customer = new CustomerBuilder();
152164

153-
Customer customer = new CustomerBuilder()
165+
Customer customer = new CustomerBuilder()
154166
.WithFirstName("Matt")
155167
.WithLastName("Kocaj")
156168
.WhoJoinedIn(2010);
157169

158170
Or to create a list of entities:
159171

160-
List<Customer> entities = BasicCustomerBuilder.CreateListOfSize(5);
172+
List<Customer> entities = CustomerBuilder.CreateListOfSize(5);
161173

162174
List<Customer> data = CustomerBuilder.CreateListOfSize(3)
163-
.All().With(b => b.WithFirstName(generator.Generate().ToString()));
175+
.TheFirst(1).WithFirstName("John");
176+
177+
Create object without requiring custom builder class
178+
----------------------------------------------------
179+
180+
If you are building domain entities or other important classes having a custom builder class with intention-revealing method (e.g. WithFirstName) provides terseness (avoiding lambda expressions) and allows the builder class to start forming documentation about the usage of that object.
181+
182+
Sometimes though, you just want to build a class without that ceremony. Typically, we find that this applies for view models and DTOs.
183+
184+
In that instance you can use the generic Builder implementation as shown below:
185+
186+
```c#
187+
StudentViewModel vm = Builder<StudentViewModel>.CreateNew()
188+
.Set(x => x.FirstName, "Pi")
189+
.Set(x => x.LastName, "Lanningham")
190+
.Set(x => x.EnrollmentDate, new DateTime(2000, 1, 1));
191+
192+
var studentViewModels = Builder<StudentViewModel>.CreateListOfSize(5)
193+
.TheFirst(1).Set(x => x.FirstName, "First")
194+
.TheNext(1).Set(x => x.LastName, "Next Last")
195+
.TheLast(1).Set(x => x.LastName, "Last Last")
196+
.ThePrevious(2).With(b => b.Set(x => x.LastName, "last" + (++i).ToString()))
197+
.All().Set(x => x.EnrollmentDate, _enrollmentDate)
198+
.BuildList();
199+
```
200+
201+
The syntax is modelled closely against what NBuilder provides and the behaviour of the class should be very similar.
202+
203+
### Customising the construction of the object
204+
205+
By default the longest constructor of the class you specify will be called and then all properties (with public and private setters) will be set with values you specified (or anonymous values if none were specified).
206+
207+
Sometimes you might not want this behaviour, in which case you can specify a custom construction factory (see build objects without calling constructor section for explanation of factories) as shown below:
208+
209+
```c#
210+
var dto = Builder<MixedAccessibilityDto>.CreateNew(new CallConstructorFactory()).Build();
211+
212+
var dtos = MixedAccessibilityDto dto = Builder<MixedAccessibilityDto>.CreateListOfSize(5, new CallConstructorFactory()).BuildList();
213+
```
214+
215+
Build objects without calling constructor
216+
-----------------------------------------
217+
218+
When you extend the `TestDataBuilder` as part of creating a custom builder you will be forced to override the abstract `BuildObject` method. You have full flexibility to call the constructor of your class directly as shown above, but you can also invoke some convention-based factories to speed up the creation of your builder (also shown above) using the `BuildUsing` method.
219+
220+
The `BuildUsing` method takes an instance of `IFactory`, of which you can create your own factory implementation that takes into account your own conventions or you can use one of the built-in ones:
221+
222+
* `AllPropertiesFactory` - Calls the longest constructor with builder values (or anonymous values if none set) based on case-insensitive match of constructor parameter names against property names and then calls the setter on all properties (public or private) with builder values (or anonymous values if none set)
223+
* `PublicPropertySettersFactory` - Calls the longest constructor with builder values (or anonymous values if none set) based on case-insensitive match of constructor parameter names against property names and then calls the setter on all properties with public setters with builder values (or anonymous values if none set)
224+
* `CallConstructorFactory` - Calls the longest constructor with builder values (or anonymous values if none set) based on case-insensitive match of constructor parameter names against property names
225+
* `AutoFixtureFactory` - Asks AutoFixture to create an anonymous instance of the class (note: does **not** use any builder values or anonymous values from Dossier)
164226

165227
Anonymous Values and Equivalence Classes
166228
----------------------------------------
@@ -220,4 +282,4 @@ If you have a suggestion for the library that can incorporate this value-add wit
220282
Contributions / Questions
221283
-------------------------
222284

223-
If you would like to contribute to this project then feel free to communicate with me via Twitter @robdmoore or alternatively submit a pull request / issue.
285+
If you would like to contribute to this project then feel free to communicate with Rob via Twitter (@robdmoore) or alternatively submit a pull request / issue.

TestStack.Dossier.Tests/Builder_CreateNewTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,24 @@ public void GivenBuilderWithModifications_WhenCallingBuildExplicitly_ShouldOverr
3636
.Set(x => x.LastName, "Lanningham")
3737
.Set(x => x.EnrollmentDate, new DateTime(2000, 1, 1));
3838

39-
var customer = builder.Build();
39+
var vm = builder.Build();
4040

41-
customer.FirstName.ShouldBe("Pi");
42-
customer.LastName.ShouldBe("Lanningham");
43-
customer.EnrollmentDate.ShouldBe(new DateTime(2000, 1, 1));
41+
vm.FirstName.ShouldBe("Pi");
42+
vm.LastName.ShouldBe("Lanningham");
43+
vm.EnrollmentDate.ShouldBe(new DateTime(2000, 1, 1));
4444
}
4545

4646
[Fact]
4747
public void GivenBuilderWithModifications_WhenCallingBuildImplicitly_ShouldOverrideValues()
4848
{
49-
StudentViewModel customer = Builder<StudentViewModel>.CreateNew()
49+
StudentViewModel vm = Builder<StudentViewModel>.CreateNew()
5050
.Set(x => x.FirstName, "Pi")
5151
.Set(x => x.LastName, "Lanningham")
5252
.Set(x => x.EnrollmentDate, new DateTime(2000, 1, 1));
5353

54-
customer.FirstName.ShouldBe("Pi");
55-
customer.LastName.ShouldBe("Lanningham");
56-
customer.EnrollmentDate.ShouldBe(new DateTime(2000, 1, 1));
54+
vm.FirstName.ShouldBe("Pi");
55+
vm.LastName.ShouldBe("Lanningham");
56+
vm.EnrollmentDate.ShouldBe(new DateTime(2000, 1, 1));
5757
}
5858

5959
[Fact]

0 commit comments

Comments
 (0)