# 九、伪造 SUT

在这本书的大部分时间里，我们一直在从为注入到 SUT 中的依赖项创建假动作的角度来探索 FakeItEasy 的 API。我们已经学习了如何创建、配置和断言对假动作依赖项的调用，以便我们可以测试我们的 SUT。

但是当我们伪造 SUT 的时候会发生什么？

很多时候，我们需要伪造 SUT，因为 SUT 正在以某种方式使用继承，而 FakeItEasy 没有其他方法参与其中(将继承作为最后手段而不是第一手段的另一个重要原因)。如果我们试图获得声明为受保护的虚拟、受保护的抽象空、受保护的覆盖和虚拟只读的成员的行为和期望，我们需要伪造 SUT 以便编写单元测试。

我需要在这里补充这个免责声明:我不建议伪造 SUT，除非你必须这样做。

如果这段代码是通过 FakeItEasy 配置在 SUT“覆盖”的代码，那么你伪造的真实代码就有可能永远不会被测试。换句话说，如果你不注意自己，你可能最终会将未经测试的代码发布到产品中。

有些读者可能不认为这有什么大不了的，但在我目前的工作中，我们几乎有 90%的单元测试覆盖率(缺失的 10%被集成测试覆盖)，我们的编程部门非常注意保持所有编写的代码都被单元测试 100%覆盖(或尽可能接近)。

话虽如此，让我们来看看如何在这一章中伪造 SUT，SUT 是继承自一个抽象类。

## 创建赝品

我们就是这样伪造我们的 SUT 的:

```cs
    var sut = A.Fake<ClassThatIsMySut>();

```

代码清单 108:创建一个假的 SUT

这个语法现在应该对您来说很熟悉了。但这一次，我们不是创造一个 SUT 使用的接口的赝品，并通过 SUT 的构造器注入，而是创造一个 SUT 本身的赝品。

这是我们将用来伪造 sut 的“霸主”:

```cs
    // Summary:
    //     Gets a configuration object allowing for further configuration of any call
    //     to the specified faked object.
    //
    // Parameters:
    //   fake:
    //     The fake to configure.
    //
    // Returns:
    //     A configuration object.
    public static IAnyCallConfigurationWithNoReturnTypeSpecified CallTo(object fake);

```

代码清单 109:伪造 sut 的 callto 霸王

注意我突出显示的区域:
`“…allowing for further configuration of any call to the specified faked object.”`

因此，即使我们在伪造 SUT，我们仍然在伪造的 SUT 中执行代码，除了我们通过 FakeItEasy 配置的任何调用。这是直接伪造 SUT 的一个重要区别，而不是创建 SUT 依赖的附属国的假象。

话虽如此，让我们通过重新设计一些与发送电子邮件相关的支持类来假造 SUT。

## 新的电子邮件发送者

让我们用我们在整本书中使用的`ISendEmail`接口建立一个新的例子，但是这次，我们将把`ISendEmail`注入一个抽象基类，它将包含实际调用`SendMail`的功能。

我们的`ISendEmail`界面:

```cs
    public interface ISendEmail
    {
        void SendMail(string from, string to, string subject, string body);
    }

```

代码清单 110:ISendEmail 接口

下面是我们的`Customer`课:

```cs
    public class Customer
    {
        public string Email { get; set; }
    }

```

代码清单 111:客户类

让我们添加一个名为`EmailBase`的新抽象类:

```cs
    public abstract class EmailBase
    {
        private readonly ISendEmail emailProvider;

        protected EmailBase(ISendEmail emailProvider)
        {
            this.emailProvider = emailProvider;
        }

        protected void SendEmailToCustomers(string subject, string body,
            List<Customer> customers)
        {
            foreach (var customer in customers)
            {
                emailProvider.SendMail(GetFromEmailAddress(), customer.Email, subject,
                    body);
            }
        }

        protected virtual string GetFromEmailAddress()
        {
            return ConfigurationManager.AppSettings["DefaultFromAddress"];
        }
    }

```

代码清单 112:抽象类 EmailBase

`Let’s take a look at what` `EmailBase` `is doing:`

*   委托发送邮件需要`ISendEmail`界面。
*   它的`SendEmailToCustomers`方法获取一个客户列表，然后对每个客户调用`SendMail`调用。
*   它包含一个名为`GetFromEmailAddress` `.`的受保护虚拟方法
*   该方法包装对`ConfigurationManager`的调用，该调用负责从配置中检索“发件人”电子邮件地址

`EmailBase`并没有给我们带来比本书大部分时间都在使用的`ISendEmail`赝品更多的功能，除了一点:它不需要调用者将“发件人”电子邮件地址传递到`SendMail`方法中。在本例中，让我们假设在我们向客户发送电子邮件的任何时候都将使用相同的“发件人”地址。

最后，这里是`ICustomerRepository`界面:

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

```

代码清单 113:icustomerrestory 接口

`GetAllCustomersWithOrderTotalsOfOneHundredOrGreater`允许我们的服务类检索已经下了一百个或更多订单的客户列表。

## 新的服务等级

接下来，我们来介绍一个服务类，`AdminEmailService`:

```cs
    public class AdminEmailService : EmailBase
    {
        private readonly ICustomerRepository customerRepository;

        public AdminEmailService(ICustomerRepository customerRepository,
            ISendEmail emailProvider)
            : base(emailProvider)
        {
            this.customerRepository = customerRepository;
        }

        public void SendPromotionalEmail(string subject, string body)
        {
            var customers = customerRepository
                .GetAllCustomersWithOrderTotalsOfOneHundredOrGreater();
            SendEmailToCustomers(subject, body, customers);
        }
    }

```

代码清单 114:管理服务类

`AdminEmailService`课程提供了一种简单的方法，向下了 100 个或更多订单的客户发送促销电子邮件。例如，我们发送的电子邮件可能包含折扣优惠券代码，因为我们想奖励我们的最高购买客户。销售部门、营销部门等可以使用该类中的功能。为了发送这些类型的电子邮件。

注意，这个类继承自`EmailBase`，并带有`ICustomerRepository`和`ISendEmail`依赖关系。但是这个类不使用`ISendEmail`依赖项；它将这种依赖传递给其超类的构造函数`EmailBase`。

## 单元测试

下面是单元测试的`SetUp`方法:

```cs
    [TestFixture]
    public class WhenSendingPromotionalEmail
    {
        private ISendEmail emailSender;
        private List<Customer> customers;
        private const string emailSubject = "EmailSubject";
        private const string emailBody = "EmailBody";
        private const string theConfiguredFromAddress = "someone@somecompany.com";

        [SetUp]
        public void Given()
        {
            customers = new List<Customer>
            {
                new Customer { Email = "customer1@email.com" },
                new Customer { Email = "customer2@email.com" }
            };

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

            emailSender = A.Fake<ISendEmail>();

            var sut = A.Fake<AdminEmailService>(x => x.WithArgumentsForConstructor(() =>     
                new AdminEmailService(customerRepository, emailSender)));
            A.CallTo(sut).Where(x => x.Method.Name == "GetFromEmailAddress")
                .WithReturnType<string>().Returns(theConfiguredFromAddress);

            sut.SendPromotionalEmail(emailSubject, emailBody);
        }
    }

```

代码清单 115:管理服务单元测试的设置

乍一看，有些东西看起来和我们在本书中所看到的单元测试非常相似。我们仍然在使用 FakeItEasy 创建`ICustomerRepository`和`ISendEmail`的赝品，但是在创建和配置代码之后，事情开始看起来非常不同。

伪造 SUT 向我们介绍了我们还没有见过的新的应用编程接口调用。让我们深入探讨每一个差异。

### 创建赝品

首先，让我们看一下我们用来创建赝品的代码:

```cs
    var sut = A.Fake<AdminEmailService>(x => x.WithArgumentsForConstructor(() =>     
        new AdminEmailService(customerRepository, emailSender)));

```

代码清单 116:创建 SUT 的赝品

由于`AdminEmailService`在其构造函数中包含两个项目，我们需要将其提供给`A.Fake<AdminEmailService>`调用。我们通过将一个 lambda 传递给做两件事的调用来实现这一点:

*   使用新关键字创建`AdminEmailService`类
*   将两个较早创建的赝品(`ICustomerRepository`和`ISendEmail`)传递给`AdminEmailService`的构造者

这两个操作是通过调用`WithArgumentsForConstructor`来完成的，然后向该调用传递一个 lambda，这要求我们“更新”SUT，并将两个必需的依赖项传递给它的构造函数。

如果我们查看重载`A.Fake<T>`创建的签名，我们将看到以下内容:

```cs
    // Summary:
    //     Creates a fake object of the type T.
    //
    // Parameters:
    //   options:
    //     A lambda where options for the built fake object can be specified.
    //
    // Type parameters:
    //   T:
    //     The type of fake object to create.
    //
    // Returns:
    //     A fake object.
    public static T Fake<T>(Action<FakeItEasy.Creation.IFakeOptionsBuilder<T>> options);

```

代码清单 117:使用 IFakeOptionsBuilder 伪造

这里注意`Fake<T>`使用的动作使用`IFakeOptionsBuilder<T>`。探索`IFakeOptionsBuilder<T>`界面，我们看到以下内容:

```cs
    public interface IFakeOptionsBuilder<T> : IHideObjectMembers
    {
        IFakeOptionsBuilder<T> Implements(Type interfaceType);
        IFakeOptionsBuilder<T> OnFakeCreated(Action<T> action);
        IFakeOptionsBuilder<T>
            WithAdditionalAttributes(IEnumerable<Reflection.Emit.CustomAttributeBuilder>
                customAttributeBuilders);
        IFakeOptionsBuilder<T> WithArgumentsForConstructor(Expression<Func<T>>
            constructorCall);
        IFakeOptionsBuilder<T> WithArgumentsForConstructor(IEnumerable<object>
           argumentsForConstructor);
        IFakeOptionsBuilderForWrappers<T> Wrapping(T wrappedInstance);
    }

```

代码清单 118:ifakoptions builder 接口

`WithArgumentsForConstructor`有多个过载。当你自己用`IFakeOptionsBuilder`打假时，请随意探索其他你可以调用的选项。

### 配置赝品

现在我们已经创建了一个 SUT 的赝品，我们使用以下代码对其进行配置:

```cs
    A.CallTo(sut).Where(x => x.Method.Name == "GetFromEmailAddress")
        .WithReturnType<string>().Returns(theConfiguredFromAddress);

```

代码清单 119:配置伪造的 SUT

由于在`AdminEmailService`中唯一发生的事情是调用`ICustomerRepository`以获得已经下了 100 个或更多订单的客户，并且由于我们已经在设置代码的前面处理了配置那些赝品，我们剩下要做的就是配置受保护的虚拟方法，`GetFromEmailAddress`。

`Where(x => x.Method.Name == "GetFromEmailAddress")`

这就是我们如何允许 FakeItEasy 掌握配置它的方法。请注意，这是通过向`Where`调用提供一个 lambda 来完成的，使用该 lambda 来查找`Method.Name`并提供方法的名称。我们还必须使用`WithReturnType<T>`来告诉 FakeItEasy，我们期望从这个调用中返回一个字符串。

我们在这里使用的`Where`方法不是 LINQ 提供给我们的`Where`方法。这是一个由 FakeItEasy 扩展提供给我们的`Where`方法，它看起来像这样:

```cs
    public static class WhereConfigurationExtensions
    {
        public static T Where<T>(this IWhereConfiguration<T> configuration,   
            Expression<Func<FakeItEasy.Core.IFakeObjectCall, bool>> predicate);
    }

```

代码清单 120:fakeitesy 中的 Where 扩展方法

`Where`扩展使用`Expression<Func<T, bool>>`根据提供的字符串找出要查找的方法。

配置代码以调用`Returns`结束，这允许我们指定我们期望从配置的方法调用中返回的值。

#### 一个重要的注解

如果此时你们中的任何人正在查看作为字符串提供给配置的方法的名称，而不是 FakeItEasy 强类型的名称，并且认为它看起来是错误的，那么从某种程度上来说，你们是对的。

当我们开始这一章时，我声明如果你能避免伪造 SUT，那就不要做。我也提到过很多次，当你伪造 SUT 的时候，你通常会处理一个继承自基类的类，并且很可能在这个基类中会有一些你需要配置的行为。

这就是我们如何使用 FakeItEasy 处理这个场景。当然，这里的危险是在编译时。如果有人在`EmailBase`上更改`GetFromEmailAddress`方法的名称，你不会在编译时知道——你只会在运行时知道。

这也没什么大不了的，因为每个人在提交代码之前都在运行他们所有的单元测试，对吗？或者，您可能已经建立了一个[持续集成](http://en.wikipedia.org:wiki:Continuous_integration)服务器，该服务器将击退由于代码无法编译而导致的失败构建，或者存在任何失败单元测试的代码。

在一个完美的世界中，是的……但是我们中的许多人在一个不完美的世界中工作，在这个世界中，开发人员不运行所有的单元测试就提交代码，并且我们没有持续集成。

如果您选择以这种方式使用 FakeItEasy，请注意这些潜在的问题。

### 断言

现在我们已经创建并配置了我们的赝品，是时候开始在单元测试的断言中使用它了。如果您还记得我们的测试设置，我们有两个客户，我们希望将电子邮件发送给他们。这是我们的第一个单元测试:

```cs
    [Test]
    public void SendsTheCorrectAmountOfTimes()
    {
        A.CallTo(() => emailSender
            .SendMail(theConfiguredFromAddress, A<string>._, emailSubject, emailBody))
                .MustHaveHappened(Repeated.Exactly.Twice);
    }

```

代码清单 121:两次调用 emailSender 的断言

您应该对这种类型的断言代码非常熟悉。请注意，我们使用的是`A<string>.Ignored`的`A<string>._`简写。由于我们设置了两个从我们的`customerRepository`山寨返回的客户，我们预计`SendMail`会被调用两次。我们怎样才能把这个测试变得更好？这样如何:

```cs
    [Test]
    public void SendsTheCorrectAmountOfTimes()
    {
        A.CallTo(() => emailSender
            .SendMail(theConfiguredFromAddress, A<string>._, emailSubject, emailBody))
                .MustHaveHappened(Repeated.Exactly.Times(customers.Count()));
    }

```

代码清单 122:使用客户。Count()而不是硬编码。两次

我们通过从我们在单元测试设置中定义的客户集合中获取计数来改进测试。

尽管如此，我们可以做得更好。

我们知道电子邮件发送两次很重要，但是如果两封电子邮件发送给了错误的客户，我想收到这些电子邮件的客户会摸不着头脑。此外，我们刚刚给了一个低购买量的客户一个他们不应该收到的折扣券代码。

让我们测试实际的电子邮件地址值:

```cs
    [Test]
    public void SendsToCorrectCustomers()
    {
        A.CallTo(() => emailSender.SendMail(theConfiguredFromAddress, customers[0].Email,
            emailSubject, emailBody)).MustHaveHappened(Repeated.Exactly.Once);
        A.CallTo(() => emailSender.SendMail(theConfiguredFromAddress, customers[1].Email,
            emailSubject, emailBody)).MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 123:测试我们是否向正确的客户发送了电子邮件

这是在测试应该测试的内容:调用`SendMail`时使用了正确的电子邮件地址。但是，看起来我们已经为客户列表硬编码了这些索引；我们已经失去了之前`customers.Count`变化的灵活性。让我们再做一次修正。

```cs
    [Test]
    public void SendsToCorrectCustomers()
    {
        foreach (var customer in customers)
            A.CallTo(() => emailSender.SendMail(theConfiguredFromAddress, customer.Email,
                emailSubject, emailBody))
                    .MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 124:为每个断言通过客户进行交互

我们遍历每个客户，并断言使用了正确的客户电子邮件地址。现在，如果我们想改变设置中的客户数量，我们就不必在测试中更改多行代码和打乱索引。

## 调用受保护属性获取器/设置器

类似于我们在前面的代码清单 119 中调用受保护的`GetFromEmailAddress`方法，也可以调用受保护的属性获取器或属性设置器。让我们探索一个代码示例来调用受保护的抽象属性 getter。

上一个示例中的接口保持不变:

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

```

代码清单 125:icustomerrestory 接口

```cs
    public interface ISendEmail
    {
        void SendMail(string from, string to, string subject, string body);
    }

```

代码清单 126:ISendEmail 接口

我们将改变前一个例子中的`EmailBase`类:

```cs
    public abstract class EmailBase
    {
        private readonly ISendEmail emailProvider;

        protected EmailBase(ISendEmail emailProvider)
        {
            this.emailProvider = emailProvider;
        }

        protected void SendEmailToCustomers(string subject, string body,
            IEnumerable<Customer> customers)
        {
            foreach (var customer in customers)
            {
                emailProvider.SendMail(FromEmailAddress, customer.Email, subject, body);
            }
        }

        protected abstract string FromEmailAddress { get; }
    }

```

代码清单 127:抽象类 EmailBase

在`EmailBase`类中，我们没有使用受保护的方法来获取 from 电子邮件地址，而是将检索`FromEmailAddress`的机制更改为受保护的抽象属性 getter。从这个基类继承的任何类都需要提供一个`FromEmailAddress`的实现。将检索`FromEmailAddress`的实现委托给继承类增加了这个类的灵活性，因为它不是通过`ConfigurationManager`使用硬编码字符串查找电子邮件地址，该字符串对于一个给定的配置只能是一个值。

| ![](img/note.png) | 注意:像这样在基类上声明受保护的抽象方法/属性是一个名为[模板方法](http://www.dofactory.com/net/template-method-design-pattern)的 GoF 设计模式。更有用的设计模式之一，它允许您将抽象类中的功能与接口的契约执行相结合，并允许从抽象类继承的每个类的功能有所不同。 |

让我们创建一个将使用`EmailBase`的类。坚持我们想要一种向客户发送促销电子邮件的方式的理念，我们将创建一个`PromotionalEmailService`类:

```cs
    public class PromotionalEmailService : EmailBase
    {
        private readonly ICustomerRepository customerRepository;

        public PromotionalEmailService(ICustomerRepository customerRepository,
            ISendEmail emailProvider) : base(emailProvider)
        {
            this.customerRepository = customerRepository;
        }

        public void SendEmail(string subject, string body)
        {
            var customers = customerRepository
                .GetAllCustomersWithOrderTotalsOfOneHundredOrGreater();
            SendEmailToCustomers(subject, body, customers);
        }

        protected override string FromEmailAddress
        {
            get { return "APromotionalEmail@somecompany.com"; }
        }
    }

```

代码清单 128:promotionalemail service 类

`PromotionalEmailService`类继承自`EmailBase`，接受`ICustomerRepository`和`ISendEmail`的依赖关系，并将`ISendEmail`传递给`EmailBase`的构造函数。这个类还被迫通过抽象类的受保护抽象属性声明来实现`FromEmailAddress` getter 属性。

让我们为这个类编写一些单元测试，看看如何使用 FakeItEasy 处理受保护的抽象属性 getter。一、测试类上的`SetUp`方法:

```cs
    public class WhenSendingPromotionalEmail
    {
        private List<Customer> customers;
        private ISendEmail emailSender;
        private const string subject = "Subject";
        private const string body = "Body";
        private const string fromAddress = "fromAddress";

        [SetUp]
        public void Given()
        {
            customers = new List<Customer>
            {
                new Customer { Email = "customer1@email.com" },
                new Customer { Email = "customer2@email.com" }
            };

            emailSender = A.Fake<ISendEmail>();
            var customerRepository = A.Fake<ICustomerRepository>();
            A.CallTo(() => customerRepository
                .GetAllCustomersWithOrderTotalsOfOneHundredOrGreater()).Returns(customers);

            var sut = A.Fake<PromotionalEmailService>(x => x.WithArgumentsForConstructor(
                () => new PromotionalEmailService(customerRepository, emailSender)));
            A.CallTo(sut).Where(x => x.Method.Name == "get_FromEmailAddress")
                .WithReturnType<string>().Returns(fromAddress);
            sut.SendEmail(subject, body);
        }
    }

```

代码清单 129:promotionalemail service 测试单元测试设置方法

在我们的`Where`子句中，为了访问`FromEmailAddress`属性，我们在要使用的属性名称前面附加了一个`get_`，以便告诉 FakeItEasy 返回什么类型和值。将`get_`附加到`FromEmailAddress`房产的前面可以让 FakeItEasy 访问该房产。

我们单元测试课的测试方法:

```cs
    [Test]
    public void SendsTheCorrectAmountOfTimes()
    {
        A.CallTo(() => emailSender
            .SendMail(fromAddress, A<string>._, subject, body))
                .MustHaveHappened(Repeated.Exactly.Times(customers.Count()));
    }

     [Test]
    public void SendsToCorrectCustomers()
    {
        foreach (var customer in customers)
        {
            A.CallTo(() => emailSender
                .SendMail(fromAddress, customer.Email, subject, body))
                    .MustHaveHappened(Repeated.Exactly.Once);  
        }
    }

```

代码清单 130:promotionalemail servicetests 类的测试方法

基于我们的设置，我们在单元测试中有两个断言。`SendsTheCorrectAmountOfTimes`确保在我们的设置中为客户数量调用`SendMail`。`SendsToCorrectCustomers`确保每封电子邮件都发给正确的客户。

现在我们已经看到了如何调用受保护的属性 getter。我们以完全相同的方式调用受保护的属性*设置器*，除了在属性名的开头添加`set_`而不是在属性名的开头添加`get_`。

在代码清单 129 中，如果`FromEmailAddress`是属性设置器而不是属性获取器，那么配置代码将如下所示:

```cs
    A.CallTo(sut).Where(x => x.Method.Name == "set_FromEmailAddress")
        .WithReturnType<string>().Returns(fromAddress);

```

代码清单 131:如果 FromEmailAddress 是一个属性设置器而不是属性获取器，配置代码会是什么样子

## 总结

在这一章中，我们已经讨论了为什么我们需要创建一个假冒的 SUT。然后，我们为假冒的 SUT 编写了单元测试，并以快速查看如何使用 FakeItEasy 获取受保护的属性结束。在下一章中，我们将看看如何使用 FakeItEasy 来测试 MVC 动作方法。