On our last project we ended up with a shared test fixture for our unit tests which gave a lot of problems. So on our current project I've looked into the builder pattern. We run our unit tests in memory on the development machines and against the database on the build server.

Currently I have a T4 template which produces for example the following builder for a Student:

public class StudentBuilder : Builder<Student, StudentBuilder>
{
    public StudentBuilder()
    {
        IsMale = true;
    }

    public StudentBuilder WithFirstName(string firstName)
    {
        this.FirstName = firstName;
        return this;
    }

    public StudentBuilder WithLastName(string lastName)
    {
        this.LastName = lastName;
        return this;
    }

    public StudentBuilder WithIsMale(bool isMale)
    {
        this.IsMale = isMale;
        return this;
    }

    internal override Student Construct()
    {
        Student result = new Student()
        {
            FirstName = FirstName ?? "FirstName:" + id.ToString(),
            LastName = LastName ?? "LastName:" + id.ToString(),
            IsMale = IsMale,
            Id = id,
        };

     /   return result;
    }
}

Trough the base classes I can use this in the following way:

Student wouter = StudentBuilder.Build()
    .WithFirstName("Wouter")
    .WithLastName("de Kort");
List<Student> students = StudentBuilder.Build().Multiple(10, (builder, index) => builder.WithFirstName("FirstName" + index));

We run integration tests on our build server to make sure everything works against the database. This means we have to make sure that all referential constrains are met. But then the problems begin.

For example, a student is required to have a mentor, a mentor belongs to a school, a school to a city, a city to a ....

This would result in code like:

StudentBuilder.Build().WithMentor(MentorBuilder.Build().WithSchool(SchoolBuilder.Build().WithCity(CityBuilder.Build()))

How should I optimize this? I've thought about doing the 'default building' in the Construct method of each Builder but if I would build 10 students then it would lead to 10 mentors in 10 schools in 10 cities in 10....

Or maybe creating methods like WithAllCity(..), WithAll(School)

Any ideas? Am I actually using the Builder Pattern the right way? Could a Director class help? Or should I have inherited classes from StudentBuilder which solve these different cases?

Or another idea, should I add more validation in my service layer before sending the data to the database? Then I would catch more errors in my unit tests against the in memory database.

有帮助吗?

解决方案

If your unit test is going to be using the student's mentor, the mentor's school, and the school's city, I think it is reasonable for the unit test to have code to build all of that, but I suggest your unit test might not be testing just one thing. Make your unit tests more specific so that they are not drilling down through so many properties.

If the problem is not your unit tests, but that your student class demands a mentor to be fed into its constructor, and that mentor cannot be null, consider relaxing that requirement to allow a null mentor (my preference I suppose), or make the builder fill in a "default" object as you say. You could even make your default objects throw exceptions if you try to access their properties, prompting you that your unit test needs you to build an "actual" object.

其他提示

If you are going to building lists of students you can make a list builder class - StudentsBuilder. By default the builder class will generate a list of Students will psuedo-random properties defined by you. This is similar to the approach of AutoPoco.

I find that making your own list builder class is more flexible in terms of defining the creation behavior and supporting any type of class. I make a builder class with IList<T> fields (similar to a data-oriented structure of arrays (SoA) approach).

public class StudentsBuilder
{
    private int _size;
    private IList<string> _firstNames; 
    private IList<string> _lastNames;
    private IList<MentorBuilder> _mentors;

    public StudentsBuilder(int size = 10)
    {
        _size = 10;
        _firstNames = new RandomStringGenerator(size).Generate();
        _lastNames = new RandomStringGenerator(size).Generate();
        _mentors = Enumerable.Range(0, size).Select(_ => new MentorBuilder()).ToList();
    }

    public StudentsBuilder WithFirstNames(params string[] firstNames)
    {
        _firstNames = firstNames;
        return this;
    }

    public IList<Student> Build()
    {
        students = new List<Student>();
        for (int i = 0; i < size; i++)
            students.Add(new Student(_firstNames[i], _lastNames[i], _mentors[i].Build());
        return students;
    }
}

Each field list is overridden using a separate method taking a params array argument. You could also make field lists public in order to use a fancier With(Action<StudentsBuilder> action) syntax for overriding values. Test code looks like:

var students = new StudentBuilder(size: 4)
    .WithFirstNames("Jim", "John", "Jerry", "Judy")
    .Build();
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top