wip-7.3

2026-06-05 ⏳1.4分钟(0.6千字)

7.3 Analysis of optimal unit test coverage

7.3 最佳单元测试覆盖率分析

Now that we’ve completed the refactoring with the help of the Humble Object pattern, let’s analyze which parts of the project fall into which code category and how those parts should be tested. Table 7.1 shows all the code from the sample project grouped by position in the types-of-code diagram.

现在,我们已经借助 Humble Object 模式完成了重构,接下来分析项目中的哪些部分分别落入哪些代码类别,以及这些部分应该如何测试。表 7.1 展示了示例项目中的所有代码,并按它们在代码类型图中的位置分组。

Table 7.1

With the full separation of business logic and orchestration at hand, it’s easy to decide which parts of the code base to unit test.

在业务逻辑与编排完全分离之后,就很容易决定代码库中哪些部分应该进行单元测试。

7.3.1 Testing the domain layer and utility code

7.3.1 测试领域层和工具代码

Testing methods in the top-left quadrant in table 7.1 provides the best results in cost-benefit terms. The code’s high complexity or domain significance guarantees great protection against regressions, while having few collaborators ensures the lowest maintenance costs. This is an example of how User could be tested:

测试表 7.1 左上象限中的方法,从成本收益角度看能获得最佳结果。代码的高复杂度或领域重要性保证了强大的防止回归能力,而协作者较少则确保了最低维护成本。下面是测试 User 的一个示例:

[Fact]
public void Changing_email_from_non_corporate_to_corporate()
{
    var company = new Company("mycorp.com", 1);
    var sut = new User(1, "user@gmail.com", UserType.Customer);

    sut.ChangeEmail("new@mycorp.com", company);

    Assert.Equal(2, company.NumberOfEmployees);
    Assert.Equal("new@mycorp.com", sut.Email);
    Assert.Equal(UserType.Employee, sut.Type);
}

To achieve full coverage, you’d need another three such tests:

要实现完整覆盖,你还需要另外三个这样的测试:

public void Changing_email_from_corporate_to_non_corporate()
public void Changing_email_without_changing_user_type()
public void Changing_email_to_the_same_one()

Tests for the other three classes would be even shorter, and you could use parameterized tests to group several test cases together:

另外三个类的测试会更短,你还可以使用参数化测试把多个测试用例分组在一起:

[InlineData("mycorp.com", "email@mycorp.com", true)]
[InlineData("mycorp.com", "email@gmail.com", false)]
[Theory]
public void Differentiates_a_corporate_email_from_non_corporate(
    string domain, string email, bool expectedResult)
{
    var sut = new Company(domain, 0);

    bool isEmailCorporate = sut.IsEmailCorporate(email);

    Assert.Equal(expectedResult, isEmailCorporate);
}

7.3.2 Testing the code from the other three quadrants

7.3.2 测试其他三个象限中的代码

Code with low complexity and few collaborators (bottom-left quadrant in table 7.1) is represented by the constructors in User and Company, such as

低复杂度且协作者较少的代码(表 7.1 左下象限)以 User 和 Company 中的构造函数为代表,例如:

public User(int userId, string email, UserType type)
{
    UserId = userId;
    Email = email;
    Type = type;
}

These constructors are trivial and aren’t worth the effort. The resulting tests wouldn’t provide great enough protection against regressions.

这些构造函数很平凡,不值得投入精力。由此产生的测试无法提供足够强的防止回归能力。

The refactoring has eliminated all code with high complexity and a large number of collaborators (top-right quadrant in table 7.1), so we have nothing to test there, either. As for the controllers quadrant (bottom-right in table 7.1), we’ll discuss testing it in the next chapter.

这次重构已经消除了所有高复杂度且协作者数量多的代码(表 7.1 右上象限),所以那里也没有什么需要测试。至于控制器象限(表 7.1 右下象限),我们会在下一章讨论如何测试它。

7.3.3 Should you test preconditions?

7.3.3 应该测试前置条件吗?

Let’s take a look at a special kind of branching points—preconditions—and see whether you should test them. For example, look at this method from Company once again:

让我们看一种特殊的分支点——前置条件——并看看你是否应该测试它们。例如,再看看 Company 中的这个方法:

public void ChangeNumberOfEmployees(int delta)
{
    Precondition.Requires(NumberOfEmployees + delta >= 0);

    NumberOfEmployees += delta;
}

It has a precondition stating that the number of employees in the company should never become negative. This precondition is a safeguard that’s activated only in exceptional cases. Such exceptional cases are usually the result of bugs. The only possible reason for the number of employees to go below zero is if there’s an error in code. The safeguard provides a mechanism for your software to fail fast and to prevent the error from spreading and being persisted in the database, where it would be much harder to deal with. Should you test such preconditions? In other words, would such tests be valuable enough to have in the test suite?

它有一个前置条件,规定公司员工数量绝不能变成负数。这个前置条件是一种保护措施,只会在异常情况下被激活。这类异常情况通常是 bug 的结果。员工数量降到零以下的唯一可能原因,是代码中存在错误。这个保护措施为软件提供了一种快速失败机制,并防止错误扩散并被持久化到数据库中;一旦进入数据库,处理起来就会困难得多。那么,你应该测试这样的前置条件吗?换句话说,这类测试是否有足够价值,值得放入测试套件?

There’s no hard rule here, but the general guideline I recommend is to test all preconditions that have domain significance. The requirement for the non-negative number of employees is such a precondition. It’s part of the Company class’s invariants: conditions that should be held true at all times. But don’t spend time testing preconditions that don’t have domain significance. For example, UserFactory has the following safeguard in its Create method:

这里没有硬性规则,但我推荐的一般准则是:测试所有具有领域重要性的前置条件。员工数量不能为负这个要求就是这样的前置条件。它是 Company 类不变量的一部分:也就是任何时候都应该保持为真的条件。但不要花时间测试没有领域重要性的前置条件。例如,UserFactory 的 Create 方法中有如下保护措施:

public static User Create(object[] data)
{
    Precondition.Requires(data.Length >= 3);

    /* Extract id, email, and type out of data */
}

There’s no domain meaning to this precondition and therefore not much value in testing it.

这个前置条件没有领域含义,因此测试它并没有多少价值。