The new Unit Test structure in ABP application
A typical ABP modular project usually consists of three main projects: Application
, Domain
, and EntityFrameworkCore/MongoDB
. In these projects, we may provide many services that require unit testing.
Using abstract unit test classes involves first writing tests in the Application
and Domain
layers that are independent of the storage technology, ensuring the correctness of core business logic. These abstract tests are then implemented in EntityFrameworkCore
or MongoDB
. The benefits of this approach include:
- Reduced Coupling: Core logic tests do not depend on specific storage technologies, so switching databases does not require rewriting test code.
- Better Isolation: Focuses on verifying business logic correctness, avoiding interference from database operations.
- Increased Reusability: The same abstract tests can be reused with different storage implementations.
- Easier Maintenance and Extensibility: Different storage implementations can be extended independently without breaking existing tests.
- Faster and More Reliable Tests: Reduces dependency on databases, making tests faster and more stable.
How to migrate old unit tests to the new unit test structure
Assume our project name is MyCompanyName.MyProjectName
.
Changes to the MyCompanyName.MyProjectName.Application.Tests
project:
- Remove the
MyCompanyName.MyProjectName.Application.Tests
project'sMyProjectNameApplicationCollection
class. - Modify the
MyCompanyName.MyProjectName.Application.Tests
project'sMyProjectNameApplicationTestBase
class.
public abstract class MyProjectNameApplicationTestBase<TStartupModule> : MyProjectNameTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
//...
}
- Modify the
MyCompanyName.MyProjectName.Application.Tests
project's unit test classes to become abstract unit test classes, such as:SampleAppServiceTests
.
public abstract class SampleAppServiceTests<TStartupModule> : MyProjectNameApplicationTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
[Fact]
public async Task Initial_Data_Should_Contain_Admin_User()
{
//...
}
}
Changes to the MyCompanyName.MyProjectName.Domain.Tests
project:
- Remove the
MyCompanyName.MyProjectName.Domain.Tests
project'sMyProjectNameDomainCollection
class. - Modify the
MyCompanyName.MyProjectName.Domain.Tests
project'sMyProjectNameDomainTestBase
class.
public abstract class MyProjectNameDomainTestBase<TStartupModule> : MyProjectNameTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
//...
}
- Modify the
MyCompanyName.MyProjectName.Domain.Tests
project's unit test classes to become abstract unit test classes, such as:SampleDomainTests
.
public abstract class SampleDomainTests<TStartupModule> : MyProjectNameDomainTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
[Fact]
public async Task Should_Set_Email_Of_A_User()
{
//...
}
}
- Modify the
MyCompanyName.MyProjectName.Domain.Tests
project'scsproj
and module class. Remove references toEntityFrameworkCore/MongoDB
.
MyCompanyName.MyProjectName.Domain.Tests.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
//...
<ItemGroup>
<ProjectReference Include="..\..\src\MyCompanyName.MyProjectName.Domain\MyCompanyName.MyProjectName.Domain.csproj" />
<ProjectReference Include="..\MyCompanyName.MyProjectName.TestBase\MyCompanyName.MyProjectName.TestBase.csproj" />
</ItemGroup>
</Project>
MyProjectNameDomainTestModule.cs
:
[DependsOn(
typeof(MyProjectNameDomainModule),
typeof(MyProjectNameTestBaseModule)
)]
public class MyProjectNameDomainTestModule : AbpModule
{
//...
}
Changes to the MyCompanyName.MyProjectName.EntityFrameworkCore.Tests
project:
Here, we need to create implementation classes for all abstract unit tests.
[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class EfCoreSampleAppServiceTests : SampleAppServiceTests<MyProjectNameEntityFrameworkCoreTestModule>
{
//...
}
[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class EfCoreSampleDomainTests : SampleDomainTests<MyProjectNameEntityFrameworkCoreTestModule>
{
//...
}
We also need to modify the project's dependencies and module class, which should directly or indirectly reference the Application
and Domain
test projects.
MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
//...
<ItemGroup>
<ProjectReference Include="..\..\src\MyCompanyName.MyProjectName.EntityFrameworkCore\MyCompanyName.MyProjectName.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj" />
<ProjectReference Include="..\..\..\..\..\framework\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj" />
</ItemGroup>
</Project>
MyProjectNameEntityFrameworkCoreTestModule.cs
:
[DependsOn(
typeof(MyProjectNameApplicationTestModule),
typeof(MyProjectNameEntityFrameworkCoreModule),
typeof(AbpEntityFrameworkCoreSqliteModule)
)]
public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule
{
//...
}
Changes to the MyCompanyName.MyProjectName.MongoDB.Tests
project (skip this step if not using MongoDB):
Like the EntityFrameworkCore
project, we need to create implementation classes for all abstract unit tests and modify the project's dependencies and module class.
[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class MongoDBSampleAppServiceTests : SampleAppServiceTests<MyProjectNameMongoDbTestModule>
{
//...
}
[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class MongoDBSampleDomainTests : SampleDomainTests<MyProjectNameMongoDbTestModule>
{
//...
}
<Project Sdk="Microsoft.NET.Sdk">
//...
<ItemGroup>
<ProjectReference Include="..\..\src\MyCompanyName.MyProjectName.MongoDB\MyCompanyName.MyProjectName.MongoDB.csproj" />
<ProjectReference Include="..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj" />
</ItemGroup>
</Project>
[DependsOn(
typeof(MyProjectNameApplicationTestModule),
typeof(MyProjectNameMongoDbModule)
)]
public class MyProjectNameMongoDbTestModule : AbpModule
{
//...
}
Changes to the MyCompanyName.MyProjectName.Web.Tests
project:
We need to reference the EntityFrameworkCore/MongoDB
test projects in this test project.
<Project Sdk="Microsoft.NET.Sdk">
//...
<ItemGroup>
<ProjectReference Include="..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj" />
<ProjectReference Include="..\..\src\MyCompanyName.MyProjectName.Web\MyCompanyName.MyProjectName.Web.csproj" />
<ProjectReference Include="..\..\..\..\..\framework\src\Volo.Abp.AspNetCore.TestBase\Volo.Abp.AspNetCore.TestBase.csproj" />
<ProjectReference Include="..\MyCompanyName.MyProjectName.EntityFrameworkCore.Tests\MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj" />
</ItemGroup>
</Project>
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(MyProjectNameWebModule),
typeof(MyProjectNameApplicationTestModule),
typeof(MyProjectNameEntityFrameworkCoreTestModule)
)]
public class MyProjectNameWebTestModule : AbpModule
{
//...
}
We no longer need the MyProjectNameWebCollection
class in this project. Please delete it and use [Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
instead.
Conclusion
This is our new unit test structure. Decoupling unit tests from storage technologies ensures the independence of business logic and allows easy switching between storage implementations. Abstract unit test classes improve test reusability, maintainability, and efficiency, reducing refactoring costs and providing flexibility for future tech updates.
Comments
Jason Ying 3 weeks ago
good ariticle! sir what's your email? I wanna disscuis some tech issues in this article with you, can I?
Jason Ying 3 weeks ago
or please contact me jason.k.ying@gmail.com, thank you !