# 二、单元测试、IoC 和桩

## 无故障单元测试

本章将引导我们尝试测试一个不可测试的类，重构以允许测试该类，为测试创建一个“桩”类，最后，使用 FakeItEasy 代替桩类。

### 设置舞台

让我们先来探索没有 FakeItEasy 的单元测试。假设我们必须为一个当前依赖于 Repository 类的类编写一个单元测试。

![](img/image014.png)如果您不熟悉存储库模式，您可以在继续之前阅读这里和这里的[部分。在很高的层次上，存储库模式从使用它的类中抽象出数据库查询和数据库代码。](https://lostechies.com/jimmybogard/2009/09/03/ddd-repository-implementation-patterns/)

下面的类`CustomerService`依赖于一个`CustomerRepository`类。

```cs
    public class CustomerService
    {
        public Customer GetCustomerByCustomerId(int customerId)
        {
            var customerRepository = new CustomerRepository();
            return customerRepository.GetCustomerBy(customerId);
        }
    }

```

代码清单 4:依赖于客户位置类的客户服务类

你可以看到我们在`GetCustomerByCustomerId`方法中创建了一个新的`CustomerRepository`，存储库使用`GetCustomerBy`方法通过`customerId`查询数据库中的`Customer`。

### 对客户服务类进行单元测试

下面是我们的单元测试:

```cs
    [TestFixture]
    public class WhenGettingCustomerById
    {
        private const int customerId = 1;
        private Customer result;

        [SetUp]
        public void Given()
        {
            var sut = new CustomerService();
            result = sut.GetCustomerByCustomerId(customerId);
        }

        [Test]
        public void ReturnsTheCorrectId()
        {
            Assert.That(result.Id, Is.EqualTo(customerId));
        }
    }

```

代码清单 5:客户服务的单元测试

当我们运行这个单元测试时，我们很可能会失败。为什么呢？因为`CustomerRepository`类可能依赖于运行时环境(例如，对真实数据库的访问)。

| ![](img/note.png) | 注意:我在这里没有展示 CustomerRepository 类的实现，因为它并不重要。客户位置与数据库通信的方式(ADO。NET、实体框架、NHibernate、RavenDb 等。)不要紧。只需知道，该类将尝试接触某种类型的数据库来查询“客户”实体。 |

我们从[坚实的原则](https://lostechies.com/chadmyers/2008/03/08/pablo-s-topic-of-the-month-march-solid-principles/)中知道，我们的`CustomerService`类不应该创建(也就是“更新”)一个`CustomerRepository`内联。我们违反了[依赖倒置原则](https://lostechies.com/jimmybogard/2008/03/31/ptom-the-dependency-inversion-principle/).)。

我们也从良好的单元测试实践中知道，我们永远不应该依赖于基础设施相关的项目，比如数据库、web 服务器和 web 服务来启动和运行我们的单元测试。

让我们通过引入[控制反转](http://stackoverflow.com/questions/3058/))来解决这个问题，看看它如何改变我们的单元测试。

### 使用控制反转移除客户位置相关性

这是我们新的`CustomerService`课:

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

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

        public Customer GetCustomerByCustomerId(int customerId)
        {
            return customerRepository.GetCustomerBy(customerId);
        }
    }

```

代码清单 6:客户服务类现在通过它的构造函数获得了一个依赖项

请注意以下更改:

*   我们在类中添加了一个构造函数，它采用了一个名为`ICustomerRepository`的接口。我们将这种依赖注入到类中。在运行时，我们将使用像微软的 Unity 或 StructureMap 这样的容器来解决依赖关系。
*   `GetCustomerByCustomerId`方法现在使用注入的接口来调用`GetCustomerBy(customerId)`。

| ![](img/note.png) | 注:关于如何使用微软 Unity 作为依赖注入容器的更多信息，请参见李嘉图·佩雷斯的[微软 Unity 简洁](http://www.syncfusion.com/resources/techportal/ebooks/microsoftunity)。 |

这里是我们的`ICustomerRepository`界面:

```cs
    public interface ICustomerRepository
    {
        Customer GetCustomerBy(int customerId);
    }

```

代码清单 ICustomerRepository 接口

### 但是我们仍然需要一个实现

我们在重构单元测试时遇到了一个问题:

```cs
    [TestFixture]
    public class WhenGettingCustomerById
    {
        private const int customerId = 1;
        private Customer result;

        [SetUp]
        public void Given()
        {
            var sut = new CustomerService(???);
            result = sut.GetCustomerByCustomerId(customerId);
        }

        [Test]
        public void ReturnsTheCorrectId()
        {
            Assert.That(result.Id, Is.EqualTo(customerId));
        }
    }

```

代码清单 8:我们需要将一个实现传递给客户服务的构造函数

你可以看到我在哪里插入了“？？?"进入`CustomerService`的构造器。我需要通过一个`ICustomerRepository`的实现，由于我不能使用真正的`CustomerRepository`，我不得不写一个实现接口的类。

我得写一些叫做桩的东西。

### 拯救树桩(某种)

编写桩类足够简单；我需要写一个实现`ICustomerRepository`的新类:

```cs
    public class CustomerRepositoryStub : ICustomerRepository
    {
        public Customer GetCustomerBy(int customerId)
        {
            return new Customer { Id = customerId };
        }
    }

```

代码清单 9:为我们的单元测试实现 ICustomerRepository 的桩类

既然桩已经写好了，我们可以在单元测试中使用它:

```cs
    [TestFixture]
    public class WhenGettingCustomerById
    {
        private const int customerId = 1;
        private Customer result;

        [SetUp]
        public void Given()
        {
            var sut = new CustomerService(new CustomerRepositoryStub());
            result = sut.GetCustomerByCustomerId(customerId);
        }

        [Test]
        public void ReturnsTheCorrectId()
        {
            Assert.That(result.Id, Is.EqualTo(customerId));
        }
    }

```

代码清单 10:向客户服务的构造函数提供一个新的客户位置桩类

注意我如何创建一个新的`CustomerRepositoryStub`类，并将其传递给`CustomerService.`的构造函数

还要注意的是，`CustomerRepositoryStub`的实现基于我传入的`GetCustomerBy`方法返回一个新的`Customer`(就像真正的`CustomerRepository`会做的那样)。

当我们运行这个测试时，它通过了。

我们采用了一个不可测试的类，使其可测试，并重构了我们的单元测试，以正确地采用接口的实现，并将其传递给`CustomerService`的构造函数。

但是我们还没有完成…

### 这是一个好的解决方案吗？

让我们在这一点上后退一步，再看看`CustomerRepositoryStub`课。这是我为一个非常简单的接口编写的一个类，它只在一个单元测试中使用。

如果我期待一个不同的`Id`、`FirstName`和`LastName`对同一个 SUT 进行不同的测试呢？我必须写更多的桩类。

如果我必须实现一个有 20 种方法的接口，并且这些方法需要根据 SUT 的不同设置而改变，会怎么样？我的桩类变大了。

如果我必须测试由`Customer`上返回的数据所指示的添加到`CustomerService`类的条件语句驱动的多个场景，会怎么样？我必须写更多的桩类。

在我目前工作的项目中，我们的系统中有将近 14，000 个单元测试。如果我为大部分测试编写桩类，如果不是所有测试的话，我们谈论的是至少 14，000 个额外的类**仅仅是为了单元测试而编写的。**

现在变化来了。界面定义发生变化。你不仅要改变真正的类实现接口(在我们的例子中，`CustomerRepository`)，还要改变你为单元测试写的所有桩类！

还有更好的办法。

## 用 FakeItEasy 进行单元测试

让我们重温一下`CustomerServiceTests`类，但是这次，我们将使用 FakeItEasy，而不是为了单元测试而编写一个桩类来实现一个接口。

下面是使用 FakeItEasy 的单元测试:

```cs
    [TestFixture]
    public class WhenGettingCustomerById
    {
        private const int customerId = 1;
        private Customer result;
        private Customer customer;

        [SetUp]
        public void Given()
        {
            customer = new Customer { Id = customerId };

            //create our Fake
            var aFakeCustomerRepository = A.Fake<ICustomerRepository>();

            //set expectations for the call to .GetCustomerBy
            A.CallTo(() => aFakeCustomerRepository.GetCustomerBy(customerId))
                .Returns(customer);

            var sut = new CustomerService(aFakeCustomerRepository);
            result = sut.GetCustomerByCustomerId(customerId);
        }

        [Test]
        public void ReturnsTheCorrectId()
        {
            Assert.That(result.Id, Is.EqualTo(customerId));
        }
    }

```

代码清单 11:使用 FakeItEasy 的客户服务单元测试

对单元测试进行了大量的修改，但是最大的修改是引入了 FakeItEasy。让我们把每一行，一次一个。

```cs
    var aFakeCustomerRepository = A.Fake<ICustomerRepository>();

```

代码清单 12:创建一个赝品

这就是你如何在 FakeItEasy 中创建一个“赝品”。与其他嘲笑框架不同，在其他框架中，您必须区分创建“桩”、“间谍”或“嘲笑”，FakeItEasy 将一切都视为赝品。在这种情况下，创建我们的赝品是非常简单的；我们要求 FakeItEasy 为我们创建一个`ICustomerRepository`的赝品。

现在我们已经创建了我们的伪，我们在伪上配置调用，允许我们在我们的 SUT 中测试某个场景。我们只是断言，我们希望返回一个具有匹配标识的客户，作为我们传递给`GetCustomerBy`方法的标识:

```cs
    A.CallTo(() => aFakeCustomerRepository.GetCustomerBy(customerId)).Returns(customer);

```

代码清单 13:配置对 false 的调用并指定返回值

`A.CallTo`允许我们配置赝品，这样我们就可以开始告诉它该怎么做。我们用假的方法调用`GetCustomerBy`方法，通过我们在单元测试中设置的`customerId`，返回一个特定的客户(也是在我们的单元测试中设置的)。`A.CallTo`允许我们设置我们的期望，即当进行这个调用时，在这个单元测试中应该发生什么。

如果我们试图将不同的`customerId`传递给`GetCustomerBy`方法，当我们在测试方法中断言匹配的客户标识时，我们的单元测试将失败。

最后，我们用`Returns`指定赝品的行为。使用`Returns`允许我们控制给定配置下我们配置的赝品会返回什么。在这种情况下，我们指定在我们的单元测试中设置的客户应该由配置的调用返回。如果我们在 FakeItEasy 调用的`Returns`部分返回一个不同的客户，我们的单元测试将会失败。

如您所见，FakeItEasy 不仅允许我们断言调用`GetCustomerBy`需要特定的`customerId`，还允许我们断言返回了特定的客户。

最后，我们把赝品传给`CustomerService`的构造者:

```cs
    var sut = new CustomerService(aFakeCustomerRepository);

```

代码清单 14:将伪造的客户位置传递给客户服务

构造器取伪，因为它把`aFakeCustomerRepository`看成是`ICustomerRepository`的实现。

剩下的单元测试和以前一样，我们的测试通过了。

通过在我们的单元测试中添加几行代码，我们引入了 FakeItEasy，设置了对 fake 的方法、参数和返回值的调用预期，并且消除了编写桩类的需要。

## 总结

在这一章中，我们从不可测试的代码开始，在引入依赖注入以允许我们的类进行单元测试后，重构为使用桩，并以对 FakeItEasy 的最终重构结束，该重构消除了编写单元测试桩类的需要。接下来，我们将看看管理如何使用 FakeItEasy 的规则。