在如何使用几个步骤测试方法的过程中苦苦挣扎

时间:2015-01-15 22:01:25

标签: unit-testing testing tdd

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

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

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

3)将用户保存到数据库

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

5)将Web服务帖子发布到第三方API

6)使用从第三方返回的值

更新用户(在步骤#3中创建)

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

在TDD世界中,我试图这么想,我应该有这样的方法吗?或者是否存在我没​​有看到的设计问题?

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

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

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

这绝对是一种思考软件开发的新方法。

3 个答案:

答案 0 :(得分:2)

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

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

修改

您在评论中提到的AccountManager类是迈向良好抽象的良好一步。它与MVC代码的其余部分位于相同的命名空间中,遗憾的是,它更容易交叉依赖关系。例如,将视图模型传递给AccountManager是依赖于"错误的"方向。

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

  

应用层

     
      
  1. UI(JavaScript / HTML / CSS)
  2.   
  3. 模型 - 视图 - 控制器(Razor / ViewModel / Navigation)
  4.   
  5. 应用程序服务(业务流程/应用程序逻辑)
  6.         

    业务层

         
        
    1. 域名服务(域[EF]模型/工作单位/交易)
    2.   
    3. WCF /第三方API(适配器/客户端代理/消息)
    4.         

      数据层

           
          
      1. 数据库
      2.         

        在这个架构中,每个项目都引用它下面的项目。

推断代码的某些内容,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自然会阻止的。

答案 1 :(得分:0)

在我看来,你正在考虑正确的事情。虽然您将所有不同的任务封装在分离的模块上,但您需要一段代码来协调所有这些内容。

这些关于评估复杂流程的测试实际上是一场噩梦,因为你最终得到了一堆模拟和设置。我认为你没有多少方法可以逃脱。

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

当我遇到这种情况时,我会尝试为更相关的场景添加测试,并省略显而易见的降低测试套装的复杂性。避免对此进行100次测试,因为您可能需要在某个时刻更改流量,这将导致100次复杂测试的更改。

这不是理想的,但我认为这是一个权衡决定。

答案 2 :(得分:0)

  

在TDD世界中,我试图这么想,我应该有这样的方法吗?或者是否存在我没​​有看到的设计问题?

你的方法很好,TDD在这里没有什么可说的。它更多的是关于设计。在编写单个面向职责的组件之后(就像你似乎已经完成的那样),你必须将它们结合起来才能实现一个用例。这样做,你经常会得到与facade类似的课程(但他们应该属于少数)。

至于测试,没有简单的方法(如果有的话)。您的设置可能比平常更长。通常有助于区分哪些依赖项将用作存根(为测试方法提供数据 - 设置)以及哪些模拟(您将断言)。如您所知,步骤1,2,5将仅用于设置。

为了使您的工作更轻松,测试更具可读性,请考虑在方法中包装某些设置配置:

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

    ...
}