领域驱动设计之单元测试最佳实践(二)

领域驱动设计之单元测试最佳实践(一)

 

介绍完了DDD案例,咱们终于能够进入主题了,本方案的测试代码基于Xunit编写,断言组件采用了FluentAssertions,相似的组件还有Shouldly。另外本案例使用了Code Contracts for .NET,若是不安装此插件,可能有个别测试不能正确Pass。html

为了实现目标中的第二点:"尽可能不Mock,包括数据库读取部分”,我尝试过3种方案:前端

一、测试代码链接真实数据库,只须要将测试数据库配置到测试项目中的web.config中,便可达到这一目标。可是该方案毕竟存在不少缺点,如:须要将测试库和正式库的更改保持同步,单元测试不利于集成在CI中,不利于团队协做等。git

二、使用SQL Lite,可是因为SQL lite自己不支持一些Linq表达式如:Skip,另外还有一些功能也没法跟Sql server保持一致,最终放弃该方案。github

三、使用测试组件Effort,能够很好的配合Entity framework使用,因为Effort内部使用了关系型内存数据库nmemory,因此很是适合运行单元测试。web

固然我仍是很是期待微软可以编写基于EF的单元测试组件。数据库

我在《我眼中的领域驱动设计》一文中提到:不要使用数据库独有的技术,如存储过程和触发器等。一方面这些逻辑都应该是Domain逻辑,另外一方面一旦使用了这些技术也就意味着咱们没法为这些逻辑编写测试。ide

1、使用Effort函数

为了可以在Castle中使用基于Effort的DbContext,须要在Castle中注册Effort:单元测试

    public class FakeDbContextInstaller:IWindsorInstaller
    {
        public const string DbConnectionKey = "FakeDbConnection";
        public const string FakeBookLibraryDbContextKey = "FakeBookLibraryDbContext";
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {

            container.Register(
                Component.For<DbConnection>().UsingFactoryMethod(DbConnectionFactory.CreateTransient)
                    .Named(DbConnectionKey)
                    .LifestylePerWebRequest()
                    );

            container.Register(Component.For<BookLibraryDbContext>()
                .DependsOn(Dependency.OnComponent(typeof(DbConnection), DbConnectionKey))
                .Named(FakeBookLibraryDbContextKey)
                .LifestylePerWebRequest()
                .IsDefault());
        }
    }

2、为测试编写场景测试

为了复用测试数据,咱们须要编写场景(Scenario),下面的文件组织结构描述了这一意图:

以用户注册为例,设计RegisterUserScenario:

    public class RegisterUserScenario : ScenarioBase
    {
        public UserModel GivingModel { get; set; }

        public Guid Id { get; private set; }

        public RegisterUserScenario(IWindsorContainer container):base(container)
        {
            GivingModel = new UserModel()
            {
                Name = "Lilei",
                Password = "Password1",
                Email = "lilei@google.com",
            };
        }

        public override void Execute()
        {
            var userService = Container.Resolve<IUserService>();
            Id = userService.Register(GivingModel);
        }
    }

场景老是提供了正确的数据,执行这样的场景老是可以获得正确的结果:

        [Fact]
        public void When_RegisterUserWithValidData_Should_CreateUser()
        {
            //Arrange
            var scenario=new RegisterUserScenario(Container);

            //Act
            scenario.Execute();

            //Assert
            var user = UserService.GetUser(scenario.Id);

            user.Name.Should().Be(scenario.GivingModel.Name);
            user.Email.Should().Be(scenario.GivingModel.Email);
        }

测试的方法名很重要,咱们在读完这个方法名以后就知道该测试是在干吗。

为了获得失败的结果,咱们须要重写Scenario中的数据,好比下面的测试:

        [Fact]
        public void When_RegisterUserWithEmptyName_Should_ThrowException()
        {
            //Arrange
            var scenario=new RegisterUserScenario(Container)
            {
                GivingModel = new UserModel()
                {
                    Name = string.Empty,
                    Email = "lilei@google.com",
                    Password = "Password1"
                }
            };

            //Act
            scenario.Invoking(s => s.Execute()).ShouldThrow<Exception>("invalid username");
        }

3、基于以前的场景编写新的场景,从而达到复用数据的目的

例如咱们须要编写“用户登陆”的测试,首先须要编写LoginScenario

    public class LoginScenario:ScenarioBase
    {
        public string Email { get; set; }
        public string Password { get; set; }

        public bool Login { get; private set; }
        public Guid Id { get; private set; }
        public LoginScenario(IWindsorContainer container) : base(container)
        {
            var registerScenario=new RegisterUserScenario(container);
            registerScenario.Execute();

            Id = registerScenario.Id;
            Email = registerScenario.GivingModel.Email;
            Password = registerScenario.GivingModel.Password;

        }

        public override void Execute()
        {
            var userService = Container.Resolve<IUserService>();

            Login=userService.Login(Email, Password);
        }
    }

在这个场景的构造函数中咱们又执行了RegisterScenario,从而达到重复利用数据的目的。

为“用户登陆”编写测试:

    public class UserLoginTests:TestBase
    {
       [Fact]
       public  void When_LoginWithInexistentEmail_Should_ThrowException()
       {
            //Arrange
            var loginScenario=new LoginScenario(Container)
            {
                Email = "other@google.com",
            };

            //Act
           loginScenario.Invoking(s => s.Execute()).ShouldThrow<ApplicationServiceException>("no such user");

       }

        [Fact]
       public void When_LoginWithWrongPassword_Should_ReturnFalse()
       {
            //Arrange
            var loginScenario=new LoginScenario(Container)
            {
                Password = "wrongPassword"
            };

            //Act
            loginScenario.Execute();

            //Assert
           loginScenario.Login.Should().BeFalse();
       } 

        [Fact]
        public void When_LoginWithCorrectPassword_Should_ReturnTrue()
        {
            //Arrange
            var loginScenario = new LoginScenario(Container);
           

            //Act
            loginScenario.Execute();

            //Assert
            loginScenario.Login.Should().BeTrue();
        }

    }

 

咱们老是须要为新的业务逻辑编写新的场景,而新的场景老是基于以前编写好的场景,整个系统的任何功能均可以用真实的测试代码来覆盖。

因为咱们在测试基类中为每一个测试都开启了单独的scope,每个测试结束都会dispose数据库。因此每个测试不管运行多少遍都是相同的效果。缺点是这些测试不能并行运行,XUnit默认以不一样的测试类为单位并行运行,咱们经过在测试类上添加相同的[Collection("IntegrationTests")]标签,从而禁用XUnit的并行运行能力。

采用该方案覆盖完毕单元测试的系统,开发者每次提交代码并保证全部单元测是都是“passed”,开发者每一次代码提交都会信心满满。

高质量的单元测试不但可以确保系统的平稳运行,更是一种有效的文档,当你读完每个场景的测试用例,你基本就可以对该业务很是熟悉了。

接近真实的单元测试还能够省去你Debug的时间,只要你编写的测试经过,基本就能够确保后台代码的可靠性。另外你能够在任什么时候候从这些测试代码中Debug进去,相比从前端界面Debug代码可以节省很多时间,一劳永逸。

 

更多具体细节请查看源码:https://git.oschina.net/richieyangs/BookLibrary.git

相关文章
相关标签/搜索