项目

集成测试

你也可以跟随 Web 应用开发教程 学习开发全栈应用,包括集成测试。

集成测试基础设施

ABP 提供了一套完整的基础设施来编写集成测试。所有 ABP 基础设施和服务都会在你的测试中正常运行。应用程序启动模板已经为你预先配置好了必要的基础设施。

数据库

启动模板配置为 EF Core 使用 内存中的 SQLite 数据库(对于 MongoDB,则使用 EphemeralMongo 库)。因此,所有的配置和查询都是针对真实的数据库执行的,你甚至可以测试数据库事务。

使用内存中的 SQLite 数据库有两个主要优点:

  • 相比于外部数据库管理系统,速度更快。
  • 它为每个测试用例创建 一个全新的数据库,因此测试之间不会相互影响。

提示:对于高级集成测试,不要使用 EF Core 的内存数据库。它不是一个真正的数据库管理系统,在细节上有很多差异。例如,它不支持事务和回滚场景,因此你无法真正测试失败的情况。而内存中的 SQLite 是一个真正的数据库管理系统,支持基本的 SQL 数据库功能。

种子数据

针对空数据库编写测试并不实用。在大多数情况下,你需要在数据库中有一些初始数据。例如,如果你编写一个测试类来查询、更新和删除产品,那么在执行测试用例之前,数据库中已经有几个产品将会很有帮助。

ABP 的 数据种子系统 是一种强大的初始化数据的方法。应用程序启动模板在 .TestBase 项目中有一个 YourProjectTestDataSeedContributor 类。你可以填充它以拥有每个测试方法都可以使用的初始数据。

示例:创建一些问题作为种子数据

using System.Threading.Tasks;
using MyProject.Issues;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;

namespace MyProject
{
    public class MyProjectTestDataSeedContributor
        : IDataSeedContributor, ITransientDependency
    {
        private readonly IIssueRepository _issueRepository;

        public MyProjectTestDataSeedContributor(IIssueRepository issueRepository)
        {
            _issueRepository = issueRepository;
        }

        public async Task SeedAsync(DataSeedContext context)
        {
            await _issueRepository.InsertAsync(
                new Issue
                {
                    Title = "测试问题一",
                    Description = "测试问题一描述",
                    AssignedUserId = TestData.User1Id
                });

            await _issueRepository.InsertAsync(
                new Issue
                {
                    Title = "测试问题二",
                    Description = "测试问题二描述",
                    AssignedUserId = TestData.User1Id
                });

            await _issueRepository.InsertAsync(
                new Issue
                {
                    Title = "测试问题三",
                    Description = "测试问题三描述",
                    AssignedUserId = TestData.User1Id
                });

            await _issueRepository.InsertAsync(
                new Issue
                {
                    Title = "测试问题四",
                    Description = "测试问题四描述",
                    AssignedUserId = TestData.User2Id
                });
        }
    }
}

同时创建一个静态类来存储用户的 Id

using System;

namespace MyProject
{
    public static class TestData
    {
        public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B");
        public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999");
    }
}

通过这种方式,我们可以使用这些已知的 Issues 和用户的 Id 来执行测试。

示例:测试领域服务

AbpIntegratedTest<T> 类(定义在 Volo.Abp.TestBase 包中)用于编写与 ABP 集成的测试。T 是用于设置和初始化应用程序的根模块类型。

应用程序启动模板 在每个测试项目中都有基类,因此你可以从这些基类派生,以简化操作。

请看重写为集成测试的 IssueManager 测试:

using System.Threading.Tasks;
using Shouldly;
using Volo.Abp;
using Xunit;

namespace MyProject.Issues
{
    public abstract class IssueManager_Integration_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
        where TStartupModule : IAbpModule
    {
        private readonly IssueManager _issueManager;
        private readonly Issue _issue;

        protected IssueManager_Integration_Tests()
        {
            _issueManager = GetRequiredService<IssueManager>();
            _issue = new Issue
            {
                Title = "测试标题",
                Description = "测试描述"
            };
        }

        [Fact]
        public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit()
        {
            // 操作 & 断言
            await Assert.ThrowsAsync<BusinessException>(async () =>
            {
                await _issueManager.AssignToUserAsync(_issue, TestData.User1Id);
            });

            _issue.AssignedUserId.ShouldBeNull();
        }

        [Fact]
        public async Task Should_Assign_An_Issue_To_A_User()
        {
            // 操作
            await _issueManager.AssignToUserAsync(_issue, TestData.User2Id);

            // 断言
            _issue.AssignedUserId.ShouldBe(TestData.User2Id);
        }
    }
}

IssueManager_Integration_Tests 类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。

  • 第一个测试方法将问题分配给用户 1,而用户 1 在数据种子代码中已经分配了 3 个问题。因此,它会抛出一个 BusinessException
  • 第二个测试方法将问题分配给用户 2,用户 2 只分配了 1 个问题。因此,该方法成功。

这个类通常位于 .Domain.Tests 项目中,因为它测试的是 .Domain 项目中的一个类。它派生自 MyProjectDomainTestBase,后者已经配置好,可以正确运行测试。

编写这样的集成测试类非常简单。另一个好处是,当你向 IssueManager 类添加另一个依赖项时,以后不需要更改测试类。

示例:测试应用服务

测试 应用服务 也没有太大不同。假设你创建了一个如下定义的 IssueAppService

using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;

namespace MyProject.Issues
{
    public class IssueAppService : ApplicationService, IIssueAppService
    {
        private readonly IIssueRepository _issueRepository;

        public IssueAppService(IIssueRepository issueRepository)
        {
            _issueRepository = issueRepository;
        }

        public async Task<List<IssueDto>> GetListAsync()
        {
            var issues = await _issueRepository.GetListAsync();

            return ObjectMapper.Map<List<Issue>, List<IssueDto>>(issues);
        }
    }
}

(假设你还定义了 IIssueAppServiceIssueDto,并在 IssueIssueDto 之间创建了 对象映射)

现在,你可以在 .Application.Tests 项目中编写一个测试类:

using System.Threading.Tasks;
using Shouldly;
using Xunit;

namespace MyProject.Issues
{
    public abstract class IssueAppService_Tests<TStartupModule> : MyProjectApplicationTestBase<TStartupModule>
        where TStartupModule : IAbpModule
    {
        private readonly IIssueAppService _issueAppService;

        protected IssueAppService_Tests()
        {
            _issueAppService = GetRequiredService<IIssueAppService>();
        }

        [Fact]
        public async Task Should_Get_All_Issues()
        {
            // 操作
            var issueDtos = await _issueAppService.GetListAsync();

            // 断言
            issueDtos.Count.ShouldBeGreaterThan(0);
        }
    }
}

IssueAppService_Tests 类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。

就是这么简单。这个测试方法测试了所有内容,包括应用服务、EF Core 映射、对象到对象映射以及仓储实现。通过这种方式,你可以完整地测试解决方案的应用层和领域层。

处理集成测试中的工作单元

ABP 的 工作单元 系统在你的应用程序中控制数据库连接和事务管理。它在编写应用程序代码时无缝工作,因此你可能没有意识到它的存在。

在 ABP 中,所有数据库操作都必须在工作单元范围内执行。当你测试一个 应用服务 方法时,工作单元的范围就是你的应用服务方法的范围。如果你测试一个 仓储 方法,工作单元的范围就是你的仓储方法的范围。

在某些情况下,你可能需要手动控制工作单元范围。考虑以下测试方法:

public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IRepository<Issue, Guid> _issueRepository;

    protected IssueRepository_Tests()
    {
        _issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
    }

    public async Task Should_Query_By_Title()
    {
        IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
        var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
        issue.ShouldNotBeNull();
    }
}

IssueRepository_Tests 类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。

我们使用 _issueRepository.GetQueryableAsync 来获取一个 IQueryable<Issue> 对象。然后,我们使用 FirstOrDefaultAsync 方法通过标题查询一个问题。此时会执行数据库查询,你会得到一个异常,提示没有活动的工作单元。

为了使该测试正常工作,你应该手动启动一个工作单元范围,如下例所示:

public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IRepository<Issue, Guid> _issueRepository;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    protected IssueRepository_Tests()
    {
        _issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
        _unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
    }

    public async Task Should_Query_By_Title()
    {
        using (var uow = _unitOfWorkManager.Begin())
        {
            IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
            var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
            issue.ShouldNotBeNull();
            await uow.CompleteAsync();
        }
    }
}

我们使用了 IUnitOfWorkManager 服务来创建一个工作单元范围,然后在该范围内调用 FirstOrDefaultAsync 方法,这样就不会再有问题了。

注意,我们测试了 FirstOrDefaultAsync 来演示工作单元问题。通常,作为一个好的原则,你应该只为你自己的代码编写测试。

你也可以使用 WithUnitOfWorkAsync 来实现相同的功能,而不是在你的测试中编写相同的 using 块。

以下是使用 WithUnitOfWorkAsync 的相同测试场景:

public abstract class IssueRepository_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IRepository<Issue, Guid> _issueRepository;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    protected IssueRepository_Tests()
    {
        _issueRepository = GetRequiredService<IRepository<Issue, Guid>>();
        _unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
    }

    public async Task Should_Query_By_Title()
    {
        await WithUnitOfWorkAsync(() =>
        {
            IQueryable<Issue> queryable = await _issueRepository.GetQueryableAsync();
            var issue = queryable.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
            issue.ShouldNotBeNull();
        });
    }
}

WithUnitOfWorkAsync 方法有多个重载,你可以根据具体需求使用。

使用 DbContext

在某些情况下,你可能希望在测试方法中直接使用 Entity Framework 的 DbContext 对象 来执行数据库操作。在这种情况下,你可以在一个工作单元内使用 IDbContextProvider<T> 服务来获取一个 DbContext 实例。

以下示例展示了如何在测试方法中创建一个 DbContext 对象:

public abstract class MyDbContext_Tests<TStartupModule> : MyProjectDomainTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IDbContextProvider<MyProjectDbContext> _dbContextProvider;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    protected IssueRepository_Tests()
    {
        _dbContextProvider = GetRequiredService<IDbContextProvider<MyProjectDbContext>>();
        _unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
    }

    public async Task Should_Query_By_Title()
    {
        using (var uow = _unitOfWorkManager.Begin())
        {
            var dbContext = await _dbContextProvider.GetDbContextAsync();
            var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "我的问题标题");
            issue.ShouldNotBeNull();
            await uow.CompleteAsync();
        }
    }
}

MyDbContext_Tests 类是一个抽象类,该类中的测试在测试资源管理器中不可见,请参阅下面的 在 EF Core 和 MongoDB 中实现单元测试 部分,了解如何在测试资源管理器中列出并运行它们。

就像我们在 处理集成测试中的工作单元 部分所做的那样,我们应该在活动的工作单元内执行 DbContext 操作。

对于 MongoDB,你可以使用 IMongoDbContextProvider<T> 服务来获取一个 DbContext 对象,并在测试方法中直接使用 MongoDB API。

在 EF Core 和 MongoDB 中实现单元测试

.Domain.Test.Application.Tests 项目中的单元测试类都是抽象类。因此,我们需要在 EF Core 或 MongoDB 测试项目中实现测试类才能运行测试,否则它们将被忽略。

下面是在 .EntityFrameworkCore.Tests 项目中实现 IssueManager_Integration_Tests 类的一个示例:

namespace MyProject.EntityFrameworkCore.Applications;

public class EfCoreIssueAppService_Tests : IssueAppService_Tests<MyProjectEntityFrameworkCoreTestModule>
{

}

通过从相关的抽象类派生,现在我们可以在测试资源管理器中看到所有测试并运行它们。

单元测试-efcore-mongodb

从文件夹结构中可以看出,所有测试都清晰地放置在相关的子文件夹中,并且通过这种分离,它们将在测试资源管理器中可见。因此,你可以清楚地看到哪些测试与哪些层和项目相关。

在本文档中