5

在我们的上一个项目中,我们最终为我们的单元测试提供了一个共享的测试夹具,这带来了很多问题。因此,在我们当前的项目中,我研究了构建器模式。我们在开发机器的内存中运行我们的单元测试,并针对构建服务器上的数据库运行我们的单元测试。

目前我有一个 T4 模板,例如为学生生成以下构建器:

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;
    }
}

通过基类,我可以通过以下方式使用它:

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

我们在构建服务器上运行集成测试,以确保一切都对数据库有效。这意味着我们必须确保满足所有引用约束。但随后问题就开始了。

例如,要求学生有导师,导师属于学校,学校对城市,城市对......

这将导致如下代码:

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

我应该如何优化这个?我曾考虑在每个 Builder 的 Construct 方法中做“默认建筑”,但如果我要建造 10 个学生,那么它将导致 10 个城市 10 个学校的 10 个导师......

或者可能创建像 WithAllCity(..)、WithAll(School) 这样的方法

有任何想法吗?我真的以正确的方式使用构建器模式吗?导演班能帮上忙吗?或者我应该从 StudentBuilder 继承类来解决这些不同的情况?

或者另一个想法,在将数据发送到数据库之前,我应该在我的服务层中添加更多验证吗?然后我会在针对内存数据库的单元测试中发现更多错误。

4

2 回答 2

1

如果您的单元测试将使用学生的导师、导师的学校和学校的城市,我认为单元测试有代码来构建所有这些是合理的,但我建议您的单元测试可能不是测试就一件事。使您的单元测试更加具体,这样它们就不会深入研究这么多属性。

如果问题不是您的单元测试,而是您的学生班级要求将导师输入其构造函数,并且该导师不能为空,请考虑放宽该要求以允许空导师(我想我的偏好),或者使builder 如您所说填写“默认”对象。如果您尝试访问默认对象的属性,您甚至可以让默认对象抛出异常,提示您单元测试需要您构建一个“实际”对象。

于 2011-11-09T09:29:29.357 回答
1

如果您要构建学生列表,您可以创建一个列表构建器类 - StudentsBuilder。默认情况下,构建器类将生成一个学生列表,将由您定义的伪随机属性。这类似于AutoPoco的方法。

我发现制作自己的列表构建器类在定义创建行为和支持任何类型的类方面更加灵活。我创建了一个带有IList<T>字段的构建器类(类似于面向数据的数组结构 (SoA) 方法)。

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;
    }
}

每个字段列表都使用采用 params 数组参数的单独方法覆盖。您还可以公开字段列表,以便使用更高级的With(Action<StudentsBuilder> action)语法来覆盖值。测试代码如下:

var students = new StudentBuilder(size: 4)
    .WithFirstNames("Jim", "John", "Jerry", "Judy")
    .Build();
于 2015-08-22T12:19:50.913 回答