在我们的最后一个项目中,我们最终为我们的单元测试提供了一个共享测试夹具,这给出了很多问题。所以在我们当前的项目中,我已经研究了构建器模式。我们在开发机器上的内存中以及构建服务器上的数据库中运行单元测试。
目前我有一个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)
这样的方法有什么想法吗?我实际上是以正确的方式使用Builder Pattern吗?导演课可以帮忙吗?或者我应该从StudentBuilder继承类来解决这些不同的情况吗?
或者另一个想法,我应该在将数据发送到数据库之前在我的服务层中添加更多验证吗?然后我会在内存数据库的单元测试中捕获更多错误。
答案 0 :(得分:1)
如果您的单元测试将使用学生的导师,导师的学校和学校的城市,我认为单元测试使用代码构建所有这些是合理的,但我建议您的单元测试可能不是只测试一件事。使您的单元测试更具体,以便它们不会在如此多的属性中向下钻取。
如果问题不是你的单元测试,而是你的学生班要求导师加入其构造函数,并且导师不能为空,请考虑放宽该要求以允许无效导师(我的偏好,我想),或者让构建器按照您的说法填写“默认”对象。如果您尝试访问其属性,甚至可以使默认对象抛出异常,提示您单元测试需要您构建“实际”对象。
答案 1 :(得分:1)
如果要构建学生列表,可以创建列表构建器类 - StudentsBuilder。默认情况下,构建器类将生成一个Student定义的伪随机属性列表。这类似于AutoPoco的方法。
我发现创建自己的list builder类在定义创建行为和支持任何类型的类方面更灵活。我创建了一个带有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;
}
}
字段的构建器类(类似于面向数据的数组结构(SoA)方法)。
With(Action<StudentsBuilder> action)
使用params数组参数的单独方法覆盖每个字段列表。您还可以将字段列表公开,以便使用更高级的var students = new StudentBuilder(size: 4)
.WithFirstNames("Jim", "John", "Jerry", "Judy")
.Build();
语法来覆盖值。测试代码如下:
class Test < ActiveRecord::Base
has_many :questions, dependent: :destroy
accepts_nested_attributes_for :questions
end