Unit testing 如何通过几个步骤测试一个方法
我有一个带有用户注册功能的MVC网站,我有一个层,我无法思考如何测试。基本上这个方法是这样的 1) 检查数据库以查看用户是否已注册 2) 将视图模型映射到实体框架模型 3) 将用户保存到数据库中 4) 向用户发送确认电子邮件 5) 向第三方API执行web服务发布 6) 使用第三方返回的值更新用户(在步骤#3中创建) 我正在为如何或应该如何测试这一点而挣扎。我已将所有步骤抽象为单独的服务,并对这些服务进行了测试,因此此方法的真正测试将是测试流。这有效吗 在TDD世界中,我正试图这样想,我应该有这样的方法吗?还是有我没有看到的设计问题 我可以编写测试,我知道如何模拟,但当我为步骤6编写测试时,我有模拟设置,返回步骤1、2和5的数据,以确保代码达到这一程度,并确保步骤6中保存的对象具有正确的状态。我的测试设置很快就会变长 如果这是应该的,那就太好了!但我觉得我错过了我的灯泡时刻 我的灯泡时刻 我喜欢基思·佩恩的回答,看着他的界面让我从一个新的角度看问题。我还观看了一个TDD逐场比赛课程(),这确实帮助我理解了这个过程。我是从内而外而不是从外而内思考这个过程Unit testing 如何通过几个步骤测试一个方法,unit-testing,testing,tdd,Unit Testing,Testing,Tdd,我有一个带有用户注册功能的MVC网站,我有一个层,我无法思考如何测试。基本上这个方法是这样的 1) 检查数据库以查看用户是否已注册 2) 将视图模型映射到实体框架模型 3) 将用户保存到数据库中 4) 向用户发送确认电子邮件 5) 向第三方API执行web服务发布 6) 使用第三方返回的值更新用户(在步骤#3中创建) 我正在为如何或应该如何测试这一点而挣扎。我已将所有步骤抽象为单独的服务,并对这些服务进行了测试,因此此方法的真正测试将是测试流。这有效吗 在TDD世界中,我正试图这样想,我应该有这
这无疑是一种新的软件开发思维方式。在我看来,您的想法是正确的。虽然您将所有不同的任务封装在不同的模块上,但您需要一段代码来协调所有这些内容 这些负责评估复杂流的测试实际上是一场噩梦,因为您最终会有一堆模拟和设置。我不认为你有很多方法可以逃避 由于测试行为非常脆弱,只要它严重依赖于内部实现,我的建议是不要花太多时间为这种方法编写测试 当我面对这种情况时,我会尝试为更相关的场景添加测试,而忽略明显的测试,以降低测试套件的复杂性。避免为此进行100个测试,因为您可能需要在某个点更改流,这将导致更改100个复杂的测试 这并不理想,但我相信这是一个折衷的决定 在TDD世界中,我正试图这样想,我应该有这样的方法吗?还是有我没有看到的设计问题 你的方法很好,TDD在这里没有什么可说的。更多的是关于设计。在编写了单一的面向责任的组件(正如您所做的那样)之后,您必须将它们一起使用以实现用例。这样做,你通常会得到和你一样的课程(但他们应该是少数) 至于测试,没有简单的方法(如果有的话)。您的设置可能比平时更长。它通常有助于区分哪些依赖项将充当存根(为测试的方法-设置提供数据),哪些依赖项充当模拟(您将对其进行断言)。正如您所注意到的,步骤1、2、5仅用于设置 <> >为了使您的工作更容易,测试更可读,请考虑在方法中包装某些安装配置:
[Test] public void UserIsSavedToDatabase()
{
UserIsNotRegistered();
ViewModelIsMappedToEntity();
...
}
困难的测试设置是一种代码气味,我想你已经看到了这一点。答案是更多的考贝尔(抽象) 这是控制器方法中的一个常见错误,这些方法既充当UI的控制器,又协调业务流程。步骤5和6可能属于同一个方法,步骤1、3和4同样应该抽象为另一个方法。实际上,控制器方法应该做的唯一一件事是从视图接收数据,将其传递给应用程序层或业务层服务,并将结果编译成新视图以显示回用户(映射) 编辑: 您在评论中提到的
AccountManager
类是实现良好抽象的良好步骤。它与MVC代码的其余部分位于同一名称空间中,不幸的是,这使得交叉依赖关系变得更容易。例如,将视图模型传递给AccountManager
是“错误”方向的依赖关系
想象一下web应用程序的理想体系结构:
应用层
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.
}
}