# 七、断言

到目前为止，我们一直专注于在 NUnit 单元测试的设置中使用 FakeItEasy。我们没有看到任何针对我们任何设置的断言。

我们都使用单元测试框架写了无数的断言。对于 NUnit，我们可以使用`Assert.That(x, Is.EqualTo(y))`断言，其中`x`和`y`是同一类型的，加上通过框架我们可以使用的许多其他类型的断言。

但是我们如何断言在一个配置好的赝品上发生了什么呢？

NUnit 拥有 SUT 的创建权，我们使用 FakeItEasy 向 SUT 提供配置好的赝品，但是在我们的设置完成后，我们如何断言在单元测试中发生了什么或没有发生什么呢？FakeItEasy 为我们提供了两个操作符来做到这一点:`MustHaveHappened`和`MustNotHaveHappened`。我们将在本章中探讨这两种方法。

## 一定发生了什么

`MustHaveHappened`言出必行。根据赝品的设置和配置，您可以使用`MustHaveHappened`断言赝品上调用了某些东西。

### 基本用法

让我们继续我们的`ISendEmail`界面示例，并对其进行一些更改，以演示如何使用`MustHaveHappened`:

```cs
    public interface ISendEmail
    {
        void SendMail();
    }

```

代码清单 ISendEmail 接口

我们还将使用一个上面有`GetAllCustomers`方法的`ICustomerRepository`接口，该接口返回一个客户列表:

```cs
    public interface ICustomerRepository
    {
        List<Customer> GetAllCustomers();
    }

```

代码清单 ICustomerRepository 接口

现在，我们将添加一个`CustomerService`类，允许我们向所有客户发送电子邮件:

```cs
    public class CustomerService
    {
        private readonly ISendEmail emailSender;
        private readonly ICustomerRepository customerRepository;

        public CustomerService(ISendEmail emailSender, ICustomerRepository
            customerRepository)
        {
            this.emailSender = emailSender;
            this.customerRepository = customerRepository;
        }

        public void SendEmailToAllCustomers()
        {
            var customers = customerRepository.GetAllCustomers();
            foreach (var customer in customers)
            {
                emailSender.SendMail();
            }
        }
    }

```

代码清单 70:客户服务类

正如你在课堂上看到的，我们正在循环从`GetAllCustomers`返回的客户，然后为每个客户调用`SendMail`。到目前为止，没有什么太复杂的。我们已经看到了几个这样的例子，当配置对伪的调用并指定伪的行为时。但是这一次，我们将断言在我们的单元测试断言中调用了`SendMail`方法。下面是单元测试代码:

```cs
    [TestFixture]
    public class WhenSendingEmailToAllCustomers
    {
        private ISendEmail emailSender;

        [SetUp]
        public void Given()
        {
            emailSender = A.Fake<ISendEmail>();

            var customerRepository = A.Fake<ICustomerRepository>();
            A.CallTo(() => customerRepository.GetAllCustomers())
                .Returns(new List<Customer> { new Customer() });

            var sut = new CustomerService(emailSender, customerRepository);
            sut.SendEmailToAllCustomers();
        }

        [Test]
        public void SendsEmail()
        {
            A.CallTo(() => emailSender.SendMail()).MustHaveHappened();
        }
    }

```

代码清单 71:断言对伪造的 SendMail 方法的调用一定发生了

看看上面的单元测试，你可以在`SendsEmail`测试方法中看到，我们断言使用`MustHaveHappened`对假的`SendMail`方法进行了调用。

### 重复

再来看看我们的`CustomerService`类单元测试，看起来很简单。但我们只是断言电话打了。如果我们想断言一个配置好的赝品被调用了不止一次，或者一定次数呢？我们可以通过将名为`Repeated`的 FakeItEasy 抽象类传递给`MustHaveHappened`方法来实现这一点。

这里是`Repeated`抽象类:

```cs
    public abstract class Repeated
    {
        protected Repeated();
        public static IRepeatSpecification AtLeast { get; }
        public static IRepeatSpecification Exactly { get; }
        public static Repeated Never { get; }
        public static IRepeatSpecification NoMoreThan { get; }
        public static Repeated Like(Expression<Func<int, bool>> repeatValidation);
    }

```

代码清单 FakeItEasy 重复抽象类

这里有大量的功能。让我们看一些例子来澄清一下。

让我们改变我们的单元测试来演示如何将`MustHaveHappened`与`Repeated`一起使用。对于本例，测试设置保持不变；只有测试方法的断言会改变:

```cs
    [Test]
    public void SendsEmail()
    {
        A.CallTo(() => emailSender.SendMail()).MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 73:测试使用重复调用一次发送邮件

注意我们如何将`Repeated.Exactly.Once`作为参数添加到`MustHaveHappened`中。`Repeated`在这里非常有用，因为我们希望确保不会向同一客户发送同一封电子邮件超过一次。`Repeated`还有助于声明对数据库的调用只进行一次，或者在循环中执行插入或更新时，声明对数据库的调用已经进行了一定次数。

说到断言调用不止一次，让我们改变我们的单元测试来反映这一点。在我们的单元测试设置中，让我们将客户存储库修改为返回两个客户，而不是一个:

```cs
    A.CallTo(() => customerRepository.GetAllCustomers())
        .Returns(new List<Customer> { new Customer(), new Customer() });

```

代码清单 74:返回两个客户而不是一个

我们还将更新我们的单元测试，以断言对`SendMail`的调用进行了两次:

```cs
    A.CallTo(() => emailSender.SendMail())
       .MustHaveHappened(Repeated.Exactly.Twice);

```

代码清单 75:使用重复的断言。没错，两次

这是一个更具体的断言，因为它是由我们正在配置的客户数量驱动的，这些客户是从我们的客户存储库假冒的`GetAllCustomers`方法返回的。如果这是我们在单元测试中追求的一件事，那就是尽可能具体地测试所有可能的场景。

我们可以通过让测试设置中用于配置和行为的数据驱动断言来改进这个测试。以下是整个更新后的测试类:

```cs
    [TestFixture]
    public class WhenSendingEmailToTwoCustomers
    {
        private ISendEmail emailSender;
        private List<Customer> customers;

        [SetUp]
        public void Given()
        {
            emailSender = A.Fake<ISendEmail>();
            customers = new List<Customer> { new Customer(), new Customer() };

            var customerRepository = A.Fake<ICustomerRepository>();
            A.CallTo(() => customerRepository.GetAllCustomers()).Returns(customers);

            var sut = new CustomerService(emailSender, customerRepository);
            sut.SendEmailToAllCustomers();
        }

        [Test]
        public void SendsTwoEmails()
        {
            A.CallTo(() => emailSender.SendMail())
                .MustHaveHappened(Repeated.Exactly.Times(customers.Count));
        }
    }

```

代码清单 76:使用重复。没错，和顾客一起。数数

我们在这里改变了两件事:

*   我们已经将客户从使用`var`的本地声明更改为字段声明，因此我们的测试方法可以访问它。
*   我们正在使用`Repeated.Exactly.Times`。与`Exactly.Once`或`Exactly.Twice`不同，`Times`为特定重复次数取一个整数。通过通过`Times`，一个`customers.Count`，我们已经允许我们的测试设置驱动我们的测试断言。

当使用 FakeItEasy 处理断言时，您应该探索`Repeated`类提供的其他选项。

## 一定什么都没发生

正如我们可以断言一个对赝品的调用发生了，并且发生了特定的次数，我们也可以断言一个对赝品的调用没有发生。在这种情况下，我们正在测试示例中的“不快乐”路径。当我们编写单元测试时，我们希望覆盖所有场景。

在上一节`MustHaveHappened`中，我们测试了向从`CustomerRepository`返回的每位客户发送电子邮件的“快乐”路径。让我们测试一封电子邮件**而不是**被发送的路径，因为没有客户从`CustomerRepository`返回。

`CustomerService`类的实现保持不变，但是我们测试的设置和断言发生了变化:

```cs
    [TestFixture]
    public class WhenSendingEmailToAllCustomersAndNoCustomersExist
    {
        private ISendEmail emailSender;

        [SetUp]
        public void Given()
        {
            emailSender = A.Fake<ISendEmail>();

            var customerRepository = A.Fake<ICustomerRepository>();
            A.CallTo((() => customerRepository.GetAllCustomers()))
                .Returns(new List<Customer>());

            var sut = new CustomerService(emailSender, customerRepository);
            sut.SendEmailToAllCustomers();
        }

        [Test]
        public void DoesNotSendAnyEmail()
        {
            A.CallTo(() => emailSender.SendMail()).MustNotHaveHappened();
        }
    }

```

代码清单 77:使用 mustnath aves 碰巧测试了对假电子邮件发送者的调用没有发生

我们在单元测试中改变了两件事:

*   在测试设置中，我们配置了假客户存储库来返回一个空的客户列表。
*   `DoesNotSendAnyEmail`现在断言通过使用`MustNotHaveHappened`并没有给假邮件发送者打电话。

为什么没有打电话？我们再来看看我们的`CustomerService`课:

```cs
    public class CustomerService
    {
        private readonly ISendEmail emailSender;
        private readonly ICustomerRepository customerRepository;

        public CustomerService(ISendEmail emailSender,
            ICustomerRepository customerRepository)
        {
            this.emailSender = emailSender;
            this.customerRepository = customerRepository;
        }

        public void SendEmailToAllCustomers()
        {
            var customers = customerRepository.GetAllCustomers();
            foreach (var customer in customers)
            {
                emailSender.SendMail();
            }
        }
    }

```

代码清单 78:客户服务类

因为我们已经将客户存储库配置为返回一个空列表，所以没有结果可以循环。正因为如此，`emailSender.SendMail`代码从未被调用。

## 总结

在本章中，我们学习了如何在 FakeItEasy 中使用断言。我们看了如何使用`MustHaveHappened`和`MustNotHaveHappened`的例子。从那里，我们看到了`Repeated`，然后重构了一个单元测试，以允许 FakeItEasy 测试设置为测试断言提供动力。既然我们知道了如何使用 FakeItEasy 断言，那么让我们来解决如何将 FakeItEasy 用于接受参数的测试方法。