Unit testing 如何通过几个步骤测试一个方法

Unit testing 如何通过几个步骤测试一个方法,unit-testing,testing,tdd,Unit Testing,Testing,Tdd,我有一个带有用户注册功能的MVC网站,我有一个层,我无法思考如何测试。基本上这个方法是这样的 1) 检查数据库以查看用户是否已注册 2) 将视图模型映射到实体框架模型 3) 将用户保存到数据库中 4) 向用户发送确认电子邮件 5) 向第三方API执行web服务发布 6) 使用第三方返回的值更新用户(在步骤#3中创建) 我正在为如何或应该如何测试这一点而挣扎。我已将所有步骤抽象为单独的服务,并对这些服务进行了测试,因此此方法的真正测试将是测试流。这有效吗 在TDD世界中,我正试图这样想,我应该有这

我有一个带有用户注册功能的MVC网站,我有一个层,我无法思考如何测试。基本上这个方法是这样的

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

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

3) 将用户保存到数据库中

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

5) 向第三方API执行web服务发布

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

我正在为如何或应该如何测试这一点而挣扎。我已将所有步骤抽象为单独的服务,并对这些服务进行了测试,因此此方法的真正测试将是测试流。这有效吗

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

我可以编写测试,我知道如何模拟,但当我为步骤6编写测试时,我有模拟设置,返回步骤1、2和5的数据,以确保代码达到这一程度,并确保步骤6中保存的对象具有正确的状态。我的测试设置很快就会变长

如果这是应该的,那就太好了!但我觉得我错过了我的灯泡时刻

我的灯泡时刻 我喜欢基思·佩恩的回答,看着他的界面让我从一个新的角度看问题。我还观看了一个TDD逐场比赛课程(),这确实帮助我理解了这个过程。我是从内而外而不是从外而内思考这个过程


这无疑是一种新的软件开发思维方式。

在我看来,您的想法是正确的。虽然您将所有不同的任务封装在不同的模块上,但您需要一段代码来协调所有这些内容

这些负责评估复杂流的测试实际上是一场噩梦,因为您最终会有一堆模拟和设置。我不认为你有很多方法可以逃避

由于测试行为非常脆弱,只要它严重依赖于内部实现,我的建议是不要花太多时间为这种方法编写测试

当我面对这种情况时,我会尝试为更相关的场景添加测试,而忽略明显的测试,以降低测试套件的复杂性。避免为此进行100个测试,因为您可能需要在某个点更改流,这将导致更改100个复杂的测试

这并不理想,但我相信这是一个折衷的决定

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

你的方法很好,TDD在这里没有什么可说的。更多的是关于设计。在编写了单一的面向责任的组件(正如您所做的那样)之后,您必须将它们一起使用以实现用例。这样做,你通常会得到和你一样的课程(但他们应该是少数)

至于测试,没有简单的方法(如果有的话)。您的设置可能比平时更长。它通常有助于区分哪些依赖项将充当存根(为测试的方法-设置提供数据),哪些依赖项充当模拟(您将对其进行断言)。正如您所注意到的,步骤1、2、5仅用于设置

<> >为了使您的工作更容易,测试更可读,请考虑在方法中包装某些安装配置:

[Test] public void UserIsSavedToDatabase()
{
    UserIsNotRegistered();
    ViewModelIsMappedToEntity();

    ...
}

困难的测试设置是一种代码气味,我想你已经看到了这一点。答案是更多的考贝尔(抽象)

这是控制器方法中的一个常见错误,这些方法既充当UI的控制器,又协调业务流程。步骤5和6可能属于同一个方法,步骤1、3和4同样应该抽象为另一个方法。实际上,控制器方法应该做的唯一一件事是从视图接收数据,将其传递给应用程序层或业务层服务,并将结果编译成新视图以显示回用户(映射)

编辑:

您在评论中提到的
AccountManager
类是实现良好抽象的良好步骤。它与MVC代码的其余部分位于同一名称空间中,不幸的是,这使得交叉依赖关系变得更容易。例如,将视图模型传递给
AccountManager
是“错误”方向的依赖关系

想象一下web应用程序的理想体系结构:

应用层

  • 用户界面(JavaScript/HTML/CSS)
  • 模型视图控制器(Razor/ViewModel/Navigation)
  • 应用程序服务(编排/应用程序逻辑)
  • 业务层

  • 域服务(域[EF]模型/工作单元/事务)
  • WCF/第三方API(适配器/客户端代理/消息)
  • 数据层

  • 数据库
  • 在此体系结构中,每个项目都引用其下方的项目

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

    除了架构问题之外,视图模型显然不适合传递,因为它总是包含支持视图渲染的数据,而这些数据对
    AccountManager
    是无用的。这还意味着AccountManager必须了解视图模型中属性的含义。视图模型类和AccountManager现在都相互依赖。这会给代码带来不必要的脆弱性

    更好的o
    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.
        }
    }