diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..506dae4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +Contributor Guidelines +====================== + +Hi! Thank you for your interest in contributing to this open source library! + +We welcome issues and pull requests. If you are starting on something major +please raise an issue first so we can discuss it with you, make sure it fits +in with the direction of the project and provide appropriate assistance :) + +We ask that you follow the following style guidelines when submitting pull +requests, to keep the code consistent and maintainable. + + - In general - follow the default ReSharper suggestions + - Do not put a single line if clause on the same line as the if statement it + belongs to; separate them with a new-line and an indent diff --git a/CONTRIBUTOR_GUIDELINES.md b/CONTRIBUTOR_GUIDELINES.md deleted file mode 100644 index 214b5b0..0000000 --- a/CONTRIBUTOR_GUIDELINES.md +++ /dev/null @@ -1,11 +0,0 @@ -Contributor Guidelines -====================== - -Hi! Thank you for your interest in contributing to this open source library! -We ask that you follow the following style guidelines when submitting pull -requests, to keep the code consistent and maintainable. - - - Do not put an if clause and it's statement on the same line: separate them - with a new-line and indent accordingly. - -(This file will be expanded as more guidelines are established by the maintainer) \ No newline at end of file diff --git a/NextVersion.txt b/NextVersion.txt index 56fea8a..a0cd9f0 100644 --- a/NextVersion.txt +++ b/NextVersion.txt @@ -1 +1 @@ -3.0.0 \ No newline at end of file +3.1.0 \ No newline at end of file diff --git a/README.md b/README.md index 70a146c..78c9fba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TestStack.Dossier -TestStack.Dossier provides you with the code infrastructure to easily and quickly generate test fixture data for your automated tests in a terse, readable and maintainable way using the Test Data Builder pattern. +TestStack.Dossier provides you with the code infrastructure to easily and quickly generate test fixture data for your automated tests in a terse, readable and maintainable way using the Test Data Builder, anonymous value and equivalence class patterns. For more information please see the [blog post](http://robdmoore.id.au/blog/2013/05/26/test-data-generation-the-right-way-object-mother-test-data-builders-nsubstitute-nbuilder/) that gives the theory behind the approach this library was intended for and the [presentation and example code](https://github.com/robdmoore/TestFixtureDataGenerationPresentation) that gives a concrete example of the usage of the library (and the theory behind it). @@ -8,11 +8,16 @@ TestStack.Dossier is integrated with NSubstitute for proxy/mock/substitute objec Prior to v2.0 this library was known as NTestDataBuilder. -## How do I get started? +## Getting started - building a single object 1. `Install-Package TestStack.Dossier` -2. Create a builder class for one of your domain objects, e.g. if you have a customer: +2. Are you building a DTO, view model, or other class you don't want to write a custom + test data builder class for? If so then check out our [generic test data builder implementation](http://dossier.teststack.net/v1.0/docs/create-object-without-requiring-custom-builder-cla) + +3. If you want to build a custom builder class, e.g. for a domain object, so you can use the builder + as documentation and also to make the experience of building that class in your tests more rich + then you need to extend the TestDataBuilder class like in the following code example: // Customer.cs @@ -46,11 +51,14 @@ Prior to v2.0 this library was known as NTestDataBuilder. // CustomerBuilder.cs + // Yep - you have to provide the custom builder type in as a generic type argument + // it's a bit weird, but necessary for the fluent chaining to work from the base class public class CustomerBuilder : TestDataBuilder { public CustomerBuilder() { - // Can set up defaults here - any that you don't set or subsequently override will have an anonymous value generated by default. + // Can set up defaults here - any that you don't set or subsequently override + // will have an anonymous value generated by default WhoJoinedIn(2013); } @@ -60,7 +68,8 @@ Prior to v2.0 this library was known as NTestDataBuilder. return Set(x => x.FirstName, firstName); } - // Note: we typically only start with the methods that are strictly needed so the builders are quick to write and aren't bloated' + // Note: we typically only start with the methods that are strictly needed so the + // builders are quick to write and aren't bloated public virtual CustomerBuilder WithLastName(string lastName) { return Set(x => x.LastName, lastName); @@ -71,27 +80,28 @@ Prior to v2.0 this library was known as NTestDataBuilder. return Set(x => x.YearJoined, yearJoined); } + // This method is optional, by default it uses `BuildUsing()` protected override Customer BuildObject() { + return BuildUsing(); + // or, if you need more control / can't use the auto-construction assumptions return new Customer( Get(x => x.FirstName), Get(x => x.LastName), Get(x => x.YearJoined) ); - // or - return BuildUsing(); } } -3. Use the builder in a test, e.g. +4. Use the builder in a test, e.g. var customer = new CustomerBuilder() .WithFirstName("Robert") .Build(); -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. +5. 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. -## How can I create a list of entities using my builders? +## Getting started - building a list of objects This library allows you to build a list of entities fluently and tersely. Here is an example: @@ -103,7 +113,7 @@ This library allows you to build a list of entities fluently and tersely. Here i .All().WhoJoinedIn(1999) .BuildList(); -This would create the following (represented as json): +This would create the following (represented as json) - note the anonymous values that are generated: [ { @@ -133,179 +143,15 @@ This would create the following (represented as json): } ] -### Castle Dynamic Proxy Generator Exception error - -If you use the list builder functionality and get the following error: - -> Castle.DynamicProxy.Generators.GeneratorException: Can not create proxy for type because it is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] attribute, because assembly is not strong-named. - -Then you either need to: - -* Make your builder class public -* Add the following to your `AssemblyInfo.cs` file: `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]` - -### Non-virtual method Invalid Operation Exception - -If you use the list builder functionality and get the following error: - -> System.InvalidOperationException: Tried to build a list with a builder who has non-virtual method. Please make on type virtual. - -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. - -## Create Entities Implicitly - -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). - -So, to create a single entity: - - Customer customer = new CustomerBuilder(); - - Customer customer = new CustomerBuilder() - .WithFirstName("Matt") - .WithLastName("Kocaj") - .WhoJoinedIn(2010); - -Or to create a list of entities: - - List entities = CustomerBuilder.CreateListOfSize(5); - - List data = CustomerBuilder.CreateListOfSize(3) - .TheFirst(1).WithFirstName("John"); - -## Create object without requiring custom builder class - -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. - -Sometimes though, you just want to build a class without that ceremony. Typically, we find that this applies for view models and DTOs. - -In that instance you can use the generic `Builder` implementation as shown below: - - StudentViewModel vm = Builder.CreateNew() - .Set(x => x.FirstName, "Pi") - .Set(x => x.LastName, "Lanningham") - .Set(x => x.EnrollmentDate, new DateTime(2000, 1, 1)); - - var studentViewModels = Builder.CreateListOfSize(5) - .TheFirst(1).Set(x => x.FirstName, "First") - .TheNext(1).Set(x => x.LastName, "Next Last") - .TheLast(1).Set(x => x.LastName, "Last Last") - .ThePrevious(2).With(b => b.Set(x => x.LastName, "last" + (++i).ToString())) - .All().Set(x => x.EnrollmentDate, _enrollmentDate) - .BuildList(); - -The syntax is modelled closely against what NBuilder provides and the behaviour of the class should be very similar. - -Note, that in the first example above, it was not necessary to call the `Build` method at the end of the method chain. This is because the `vm` variable has been defined as `StudentViewModel` and the C# compiler is able to infer the type and the object is set *implicitly*. - -In the second example, the `var` keyword is used to define `studentViewModels`, and so it is necessary to *explicitly* call `BuildList` to set the variable. - -### Customising the construction of the object - -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). - -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: - - var dto = Builder - .CreateNew(new CallConstructorFactory()) - .Build(); - - var dtos = MixedAccessibilityDto dto = Builder - .CreateListOfSize(5, new CallConstructorFactory()) - .BuildList(); -## Build objects without calling constructor +The same works with the generic `Builder` implementation too. -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. +## Documentation -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: - -* `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) -* `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) -* `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 -* `AutoFixtureFactory` - Asks AutoFixture to create an anonymous instance of the class (note: does **not** use any builder values or anonymous values from Dossier) - -## Propagating the anonymous value fixture across builders - -Within a particular instance of `AnonymousValueFixture`, which is created for every builder, any generators that return a sequence of values (e.g. unique values) will be maintained. If you want to ensure that the same anonymous value fixture is used across multiple related builders then: - -* Using `CreateListOfSize` will automatically propagate the anonymous value fixture across builders -* Call the `GetChildBuilder(Func modifier = null)` method from within your custom builder, e.g.: - - public MyCustomBuilder WithSomeValue(Func modifier = null) - { - return Set(x => x.SomeValue, GetChildBuilder(modifier)); - } -* If using `Builder` then call the `SetUsingBuilder` method, e.g.: - - // Uses Builder - Builder.CreateNew() - .SetUsingBuilder(x => x.Address) - .Build() - // Uses Builder, includes customisation - Builder.CreateNew() - .SetUsingBuilder(x => x.Address, b => b.Set(y => y.Street, "A street")) - .Build() - // Uses AddressBuilder - Builder.CreateNew() - .SetUsingBuilder(x => x.Address) - .Build() - // Uses AddressBuilder, includes customisation - Builder.CreateNew() - .SetUsingBuilder(x => x.Address, b => b.Set(y => y.Street, "A street")) - .Build() - -There is currently no way to share an anonymous value fixture across unrelated builder instances. If this is something you need please raise an issue so we can discuss your requirement. - -## Anonymous Values and Equivalence Classes - -todo: Coming soon! - -## How can I create proxy objects? - -This library integrates with [NSubstitute](http://nsubstitute.github.io/) for generating proxy objects, this means you can call the `AsProxy` method on your builder to request that the result from calling `Build` will be an NSubstitute proxy with the public properties set to return the values you have specified via your builder, e.g. - - var customer = CustomerBuilder.WithFirstName("Rob").AsProxy().Build(); - customer.CustomerForHowManyYears(Arg.Any()).Returns(10); - var name = customer.FirstName; // "Rob" - var years = customer.CustomerForHowManyYears(DateTime.Now); // 10 - -If you need to alter the proxy before calling `Build` to add complex behaviours that can't be expressed by the default public properties returns values then you can override the `AlterProxy` method in your builder, e.g. - - class CustomerBuilder : TestDataBuilder - { - // ... - - private int _years; - - public CustomerBuilder HasBeenMemberForYears(int years) - { - _years = years; - return this; - } - - protected override void AlterProxy(Customer proxy) - { - proxy.CustomerForHowManyYears(Arg.Any()).Returns(_years); - } - - // ... - } - - // Then in your test you can use: - - var customer = new CustomerBuilder().AsProxy().HasBeenMemberForYears(10); - var years = customer.CustomerForHowManyYears(DateTime.Now); // 10 - -*Remember that when using proxy objects of real classes that you need to mark properties and methods as virtual and have a protected empty constructor.* - -## Why does TestStack.Dossier have NSubstitute and AutoFixture as dependencies? - -TestStack.Dossier is an opinionated framework and as such prescribes how to build your fixture data, including how to build lists, anonymous data and mock objects. Because of this we have decided to bundle it with the best of breed libraries for this purpose: AutoFixture and NSubstitute. - -This allows for this library to provide a rich value-add on top of the basics of tracking properties in a dictionary in the `TestDataBuilder` base class. If you want to use different libraries or want a cut down version that doesn't come with NSubstitute or AutoFixture and the extra functionality they bring then take the `TestDataBuilder.cs` file and cut out the bits you don't want - open source ftw :). - -If you have a suggestion for the library that can incorporate this value-add without bundling these libraries feel free to submit a pull request. +More comprehensive documentation is available on our [documentation website](http://dossier.teststack.net/). ## Contributions / Questions -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. +If you would like to contribute to this project then feel free to communicate with us via Twitter ([@teststacknet](https://twitter.com/teststacknet)) or alternatively submit a [pull request](https://github.com/TestStack/TestStack.Dossier/compare/) / [issue](https://github.com/TestStack/TestStack.Dossier/issues/new). + +Feel free to check out our [up-for-grabs issues if you don't know where to start](https://github.com/TestStack/TestStack.Dossier/labels/up-for-grabs). \ No newline at end of file diff --git a/TestStack.Dossier.Tests/BuildTests.cs b/TestStack.Dossier.Tests/BuildTests.cs index 010376a..8067a7a 100644 --- a/TestStack.Dossier.Tests/BuildTests.cs +++ b/TestStack.Dossier.Tests/BuildTests.cs @@ -48,32 +48,6 @@ public void GivenBuilder_WhenCallingSetExplicitly_ShouldOverrideValues() customer.YearJoined.ShouldBe(2014); } - [Fact] - public void GivenBuilder_WhenCallingSetWithLambda_ShouldInvokeEachTime() - { - int counter = 2014; - var builder = new CustomerBuilder() - .Set(x => x.FirstName, "Pi") - .Set(x => x.LastName, "Lanningham") - .Set(x => x.YearJoined, () => counter++); - - var customerA = builder.Build(); - var customerB = builder.Build(); - - customerA.YearJoined.ShouldBe(2014); - customerB.YearJoined.ShouldBe(2015); - - List customerList = CustomerBuilder.CreateListOfSize(10) - .All() - .Set(x => x.YearJoined, () => counter++); - int newCounter = 2016; - foreach (var c in customerList) - { - c.YearJoined.ShouldBe(newCounter++); - } - - } - [Fact] public void GivenBasicBuilder_WhenCallingBuildImplicitly_ThenReturnAnObject() { diff --git a/TestStack.Dossier.Tests/Builder_CreateListTests.cs b/TestStack.Dossier.Tests/Builder_CreateListTests.cs index 5ad8c51..62be8bc 100644 --- a/TestStack.Dossier.Tests/Builder_CreateListTests.cs +++ b/TestStack.Dossier.Tests/Builder_CreateListTests.cs @@ -170,7 +170,7 @@ public void WhenBuildingObjectsImplicitly_ThenTheAnonymousValueFixtureIsSharedAc studentViewModels.Select(x => x.Grade).ShouldBeUnique(); } - public void WhenBuildingObjectsWithCtorAndPrivateSetters_ShouldSetPrivateSettersByDefault() + public void WhenBuildingObjectsWithCtorAndPrivateSetters_ThenSetPrivateSettersByDefault() { var dto = Builder.CreateListOfSize(1) .TheFirst(1) @@ -188,7 +188,7 @@ public void WhenBuildingObjectsWithCtorAndPrivateSetters_ShouldSetPrivateSetters } [Fact] - public void GivenBuilderListWithFactoryOverride_WhenBuildingObjects_ShouldRespectOverriddenFactory() + public void GivenBuilderListWithFactoryOverride_WhenBuildingObjects_ThenRespectOverriddenFactory() { var dto = Builder.CreateListOfSize(1, new CallConstructorFactory()) .TheFirst(1) @@ -204,5 +204,18 @@ public void GivenBuilderListWithFactoryOverride_WhenBuildingObjects_ShouldRespec dto.NotSetByCtorWithPrivateSetter.ShouldNotBe("3"); dto.NotSetByCtorWithPublicSetter.ShouldNotBe("4"); } + + [Fact] + public void GivenBuilder_WhenCallingSetWithLambda_ThenInvokeEachTime() + { + var grade = Grade.A; + var customers = Builder.CreateListOfSize(10) + .All().Set(x => x.Grade, () => grade++) + .BuildList(); + + var gradeAssertion = Grade.A; + foreach (var c in customers) + c.Grade.ShouldBe(gradeAssertion++); + } } } \ No newline at end of file diff --git a/TestStack.Dossier.Tests/Builder_CreateNewTests.cs b/TestStack.Dossier.Tests/Builder_CreateNewTests.cs index 3581856..de36834 100644 --- a/TestStack.Dossier.Tests/Builder_CreateNewTests.cs +++ b/TestStack.Dossier.Tests/Builder_CreateNewTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Shouldly; using TestStack.Dossier.Factories; using TestStack.Dossier.Tests.TestHelpers.Objects.Examples; @@ -29,7 +30,7 @@ public void GivenBuilder_WhenCallingBuildImplicitly_ThenReturnAnObject() } [Fact] - public void GivenBuilderWithModifications_WhenCallingBuildExplicitly_ShouldOverrideValues() + public void GivenBuilderWithModifications_WhenCallingBuildExplicitly_ThenOverrideValues() { var builder = Builder.CreateNew() .Set(x => x.FirstName, "Pi") @@ -44,7 +45,7 @@ public void GivenBuilderWithModifications_WhenCallingBuildExplicitly_ShouldOverr } [Fact] - public void GivenBuilderWithModifications_WhenCallingBuildImplicitly_ShouldOverrideValues() + public void GivenBuilderWithModifications_WhenCallingBuildImplicitly_ThenOverrideValues() { StudentViewModel vm = Builder.CreateNew() .Set(x => x.FirstName, "Pi") @@ -57,7 +58,7 @@ public void GivenBuilderWithModifications_WhenCallingBuildImplicitly_ShouldOverr } [Fact] - public void GivenBuilder_WhenBuildingObjectWithCtorAndPrivateSetters_ShouldSetPrivateSettersByDefault() + public void GivenBuilder_WhenBuildingObjectWithCtorAndPrivateSetters_ThenSetPrivateSettersByDefault() { MixedAccessibilityDto dto = Builder.CreateNew() .Set(x => x.SetByCtorWithPublicSetter, "1") @@ -72,7 +73,7 @@ public void GivenBuilder_WhenBuildingObjectWithCtorAndPrivateSetters_ShouldSetPr } [Fact] - public void GivenBuilderWithFactoryOverride_WhenBuildingObject_ShouldRespectOverriddenFactory() + public void GivenBuilderWithFactoryOverride_WhenBuildingObject_ThenRespectOverriddenFactory() { MixedAccessibilityDto dto = Builder.CreateNew(new CallConstructorFactory()) .Set(x => x.SetByCtorWithPublicSetter, "1") @@ -85,5 +86,21 @@ public void GivenBuilderWithFactoryOverride_WhenBuildingObject_ShouldRespectOver dto.NotSetByCtorWithPrivateSetter.ShouldNotBe("3"); dto.NotSetByCtorWithPublicSetter.ShouldNotBe("4"); } + + [Fact] + public void GivenBuilder_WhenCallingSetWithLambda_ThenInvokeEachTime() + { + var grade = Grade.A; + var builder = Builder.CreateNew() + .Set(x => x.FirstName, "Pi") + .Set(x => x.LastName, "Lanningham") + .Set(x => x.Grade, () => grade++); + + var customerA = builder.Build(); + var customerB = builder.Build(); + + customerA.Grade.ShouldBe(Grade.A); + customerB.Grade.ShouldBe(Grade.B); + } } } \ No newline at end of file