单元测试 – 在如何使用多个步骤测试方法的过程中苦苦挣扎

我有一个带有用户注册功能的MVC网站,我有一个层,我无法围绕如何测试.基本上这个方法做到了……

1)检查数据库以查看用户是否已注册

2)将视图模型映射到实体框架模型

3)将用户保存到数据库

4)向用户发送确认电子邮件

5)对第三方API执行Web服务发布

6)使用从第三方返回的值更新用户(在步骤#3中创建)

我正在努力研究如何或应该测试这个.我已将所有步骤抽象为单独的服务,并且我已对这些测试进行了测试,因此对此方法的测试将测试流程.这有效吗?

在TDD世界中,我想这样想,我应该有这样的方法吗?还是有一个我没看到的设计问题?

我可以编写测试并且我理解如何模拟,但是当我为第6步编写测试时,我有模拟设置,返回步骤#1,#2和#5的数据,以确保代码获得那么远并确保对象在步骤#6中保存的状态正确.我的测试设置很快就会变长.

如果这是它应该是如此伟大的!但我觉得我错过了我的灯泡时刻.

我的灯泡时刻
我喜欢Keith Payne的回答,看着他的界面让我从新的角度看待事物.我还观看了TDD Play by Play课程(http://www.pluralsight.com/courses/play-by-play-wilson-tdd),这真的帮助我理解了这个过程.我从内到外思考过程,而不是从外面思考过程.

这绝对是思考软件开发的一种新方式.

最佳答案 困难的测试设置是代码味道,我想你已经看到了这一点.答案是更多的牛铃(抽象).

这是控制器方法中的常见错误,充当UI的控制器和编排业务流程.步骤5& 6可能属于一起,步骤1,3和& 4同样应该抽象到另一种方法.控制器方法应该做的唯一事情就是从视图接收数据,将其交给应用程序或业务层服务,并将结果编译成新视图以显示回用户(映射).

编辑:

您在注释中提到的AccountManager类是实现良好抽象的一个很好的步骤.它与MVC代码的其余部分位于相同的命名空间中,遗憾的是,它更容易交叉依赖关系.例如,将视图模型传递给AccountManager是“错误”方向的依赖关系.

想象一下这个Web应用程序的理想化架构:

Application Layer

  1. UI (JavaScript/HTML/CSS)
  2. Model-View-Controller (Razor/ViewModel/Navigation)
  3. Application Services (Orchestration/Application Logic)

Business Layer

  1. Domain Services (Domain [EF] Models/Unit Of Work/Transactions)
  2. WCF/Third Party API’s (Adapters/Client Proxies/Messages)

Data Layer

  1. Database

In this architecture, each item references the item below it.

推断出代码的一些内容,AccountManager最高的应用程序服务(在引用层次结构中).我不认为它在逻辑上是MVC或UI组件的一部分.现在,如果这些体系结构项位于不同的dll中,IDE将不允许您将视图模型传递到AccountManager的方法中.它会导致循环依赖.

除了体系结构问题之外,很明显视图模型不适合传递,因为它总是包含支持对AccountManager无用的视图呈现的数据.这也意味着AccountManager必须了解视图模型中属性的含义.视图模型类和AccountManager现在都相互依赖.这为代码带来了不必要的脆弱性和脆弱性.

更好的选择是传递简单的参数,或者如果您愿意,将它们打包到新的数据传输对象(DTO)中,该对象将在与AccountManager相同的位置由合同定义.

一些示例接口:

namespace MyApp.Application.Services
{
    // This component lives in the Application Service layer and is responsible for orchestrating calls into the
    // business layer services and anything else that is specific to the application but not the overall business domain.

    // For instance, sending of a confirmation email is probably a requirement in some application process flows, but not
    // necessarily applicable to every instance of adding a user to the system from every source. Perhaps there is an admin back-end
    // application which may or may not send the email when an administrator registers a new user. So that back-end 
    // application would have a different orchestration component that included a parameter to indicate whether to 
    // send the email, or to send it to more than one recipient, etc.

    interface IAccountManager
    {
        bool RegisterNewUser(string username, string password, string confirmationEmailAddress, ...);
    }
}

namespace MyApp.Domain.Services
{
    // This is the business-layer component for registering a new user. It will orchestrate the
    // mapping to EF models, calling into the database, and calls out to the third-party API.

    // This is the public-facing interface. Implementation of this interface will make calls
    // to a INewUserRegistrator and IExternalNewUserRegistrator components.

    public interface IUserRegistrationService
    {
        NewUserRegistrationResult RegisterNewUser(string username, string password, ...);
    }

    public class NewUserRegistrationResult
    {
        public bool IsUserRegistered { get; set; }
        public int? NewUserId { get; set; }

        // Add additional properties for data that is available after
        // the user is registered. This includes all available relevant information
        // which serves a distinctly different purpose than that of the data returned
        // from the adapter (see below).
    }

    internal interface INewUserRegistrator
    {
        // The implementation of this interface will add the user to the database (or DbContext)
        // Alternatively, this could be a repository 
        User RegisterNewUser(User newUser) ;
    }

    internal interface IExternalNewUserRegistrator
    {
        // Call the adapter for the API and update the user registration (steps 5 & 6)
        // Replace the return type with a class if more detailed information is required

        bool UpdateUserRegistrationFromExternalSystem(User newUser);
    }

    // Note: This is an adapter, the purpose of which is to isolate details of the third-party API
    // from yor application. This means that what comes out from the adapter is determined not by what
    // is provided by the third party API but rather what is needed by the consumer. Oftentimes these
    // are similar.

    // An example of a difference can be some mundance detail. For instance, say that the API
    // returns -1 for some non-nullable int value when the intent is to indicate lack of a match.
    // The adapter would protect the application from that detail by using some logic to interpret
    // the -1 value and set a bool to indicate that no match was found, and to use int?
    // with a null value instead of propagating the magic number (-1) throughout your application.

    internal interface IThirdPartyUserRegistrationAdapter
    {
        // Call the API and interpret the response from the API.
        // Also perform any logging, exception handling, etc.
        AdapterResult RegisterUser(...);
    }

    internal class AdapterResult
    {
        public bool IsSuccessful { get; set; }

        // Additional properties for the response data that is needed by your application only.
        // Do not include data provided by the API response that is not used.
    }
}

需要记住的是,这种设计 – 一次性与TDD相反.在TDD中,当您从外向内测试和编写代码时,对这些抽象的需求变得明显.我在这里所做的就是跳过所有这些并直接跳到设计内部工作的基础上我脑海中的图片.在几乎所有情况下,这都会导致过度设计和过度抽象,这是TDD自然会阻止的.

点赞