【问题标题】:Struggling with how test a method with several steps挣扎于如何通过几个步骤测试方法
【发布时间】:2015-01-15 22:01:24
【问题描述】:

我有一个具有用户注册功能的 MVC 网站,并且我有一层我无法理解如何测试。基本上该方法是这样做的......

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

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

3) 将用户保存到数据库

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

5) 向第 3 方 API 执行 Web 服务发布

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

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

在 TDD 世界中,我正在尝试以这种方式思考,我应该有这样的方法吗?还是有我没有看到的设计问题?

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

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

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

这绝对是一种全新的软件开发思维方式。

【问题讨论】:

    标签: unit-testing testing tdd


    【解决方案1】:

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

    这是作为 UI 控制器和编排业务流程的控制器方法中的常见错误。第 5 步和第 6 步可能属于一起,第 1、3 和 4 步同样应该抽象到另一种方法。实际上,控制器方法应该做的唯一一件事就是从视图接收数据,将其传递给应用程序或业务层服务,然后将结果编译到新视图中以显示给用户(映射)。

    编辑:

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

    想象一下这个理想化的网络应用架构:

    应用层

    1. 用户界面(JavaScript/HTML/CSS)
    2. 模型-视图-控制器(Razor/ViewModel/Navigation)
    3. 应用服务(编排/应用逻辑)

    业务层

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

    数据层

    1. 数据库

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

    推断您的代码的一些事情,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 自然会防止这种情况发生。

    【讨论】:

    • 我的 AccountController 检查模型状态,如果一切正常,则将视图模型传递给我的 AccountManager,当涉及到名称时,那个人会编排我拥有的所有内容。请您编辑您的回复并包含一些界面签名,以便我能得到更好的主意吗?
    • 我添加了接口和更广泛的解释
    【解决方案2】:

    在我看来,你的想法是对的。尽管您将所有不同的任务封装在单独的模块上,但您需要一段代码来协调所有这些内容。

    这些负责评估复杂流程的测试真的是一场噩梦,因为你最终会得到一堆模拟和设置。我不认为你有很多逃脱的方法。

    由于测试行为在很大程度上依赖于内部实现,因此非常脆弱,我的建议是不要花太多时间为此方法编写测试。

    当我遇到这种情况时,我会尝试为更相关的场景添加测试并省略明显的测试以降低测试套件的复杂性。避免为此进行 100 次测试,因为您可能需要在某些时候更改流程,这将导致 100 次复杂测试被更改。

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

    【讨论】:

    • 相关场景很容易测试:)
    【解决方案3】:

    在 TDD 世界中,我正在尝试以这种方式思考,我应该有这样的方法吗?还是有我没有看到的设计问题?

    你的方法很好,TDD在这里没什么好说的。更多的是关于设计。在编写了单一面向职责的组件(正如您似乎已经完成的那样)之后,您必须将它们一起使用来实现一个用例。这样做你通常会得到像你一样的 facade 类(但它们应该是少数)。

    至于测试,没有简单的方法(如果有的话)。您的设置可能比平时要长。它通常有助于区分哪些依赖项将用作 stubs(为测试的方法提供数据 - 设置),哪些依赖项用作 mocks(您将反对)。如您所见,步骤 1、2、5 仅用于设置。

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

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

    【讨论】:

    • 你说他们应该是少数,但如果我有一个逻辑操作(如我所描述的)并且一个应用程序往往有很多逻辑操作,我会有很多这类方法.有什么选择?
    • @oliwa:应用程序往往有很多业务案例,这就是您需要这些外观的地方。如果您的用户可以单击运行整个过程,那么别无选择。您可以添加图层、组合事物、提取到其他组件,但最终您总是需要一个地方将事物连接在一起。如果你有太多这样的类,那么你可能没有足够抽象,并且不应该是外观类(即可以很容易地抽象)的组件正在这样展示。
    猜你喜欢
    • 1970-01-01
    • 2023-04-06
    • 2014-08-30
    • 1970-01-01
    • 2019-07-17
    • 1970-01-01
    • 1970-01-01
    • 2015-08-24
    • 2021-08-05
    相关资源
    最近更新 更多