ABP (ASP.NET Boilerplate) 應用程式開發框架 新手教學 No.8 單元測試

ABP (ASP.NET Boilerplate) 應用程式開發框架 新手教學 No.8 單元測試

ABP (ASP.NET Boilerplate) 應用程式開發框架 新手教學 No.0 索引


ABP也提供了單元測試,位於方案Test資料夾內的MyCompany.MyProject.Tests測試專案

首先我們需要一個假的資料庫,這邊樣板專案其實已經幫我們建立好了

但我們還需要修改一下,在裡面加上我們先前做好的SeedData,這樣測試資料庫裡面才會有我們後來新增的預設資料

修改 Test\MyCompany.MyProject.Tests\MyProjectTestBase.cs

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using Abp;
using Abp.Configuration.Startup;
using Abp.Domain.Uow;
using Abp.Runtime.Session;
using Abp.TestBase;
using MyCompany.MyProject.EntityFramework;
using MyCompany.MyProject.Migrations.SeedData;
using MyCompany.MyProject.MultiTenancy;
using MyCompany.MyProject.Users;
using Castle.MicroKernel.Registration;
using Effort;
using EntityFramework.DynamicFilters;

namespace MyCompany.MyProject.Tests
{
    public abstract class MyProjectTestBase : AbpIntegratedTestBase<MyProjectTestModule>
    {
        private DbConnection _hostDb;
        private Dictionary<int, DbConnection> _tenantDbs; //only used for db per tenant architecture

        protected MyProjectTestBase()
        {
            //Seed initial data for host
            AbpSession.TenantId = null;
            UsingDbContext(context =>
            {
                new InitialHostDbBuilder(context).Create();
                new DefaultTenantCreator(context).Create();
            });

            //Seed initial data for default tenant
            AbpSession.TenantId = 1;
            UsingDbContext(context =>
            {
                new TenantRoleAndUserBuilder(context, 1).Create();
            });

            //Seed initial data for default map
            UsingDbContext(context =>
            {
                new PlayerAndMapBuilder(context).Create();
            });

            LoginAsDefaultTenantAdmin();
        }

        protected override void PreInitialize()
        {
            base.PreInitialize();

            /* You can switch database architecture here: */
            UseSingleDatabase();
            //UseDatabasePerTenant();
        }

        /* Uses single database for host and all tenants.
         */
        private void UseSingleDatabase()
        {
            _hostDb = DbConnectionFactory.CreateTransient();

            LocalIocManager.IocContainer.Register(
                Component.For<DbConnection>()
                    .UsingFactoryMethod(() => _hostDb)
                    .LifestyleSingleton()
                );
        }

        /* Uses single database for host and Default tenant,
         * but dedicated databases for all other tenants.
         */
        private void UseDatabasePerTenant()
        {
            _hostDb = DbConnectionFactory.CreateTransient();
            _tenantDbs = new Dictionary<int, DbConnection>();

            LocalIocManager.IocContainer.Register(
                Component.For<DbConnection>()
                    .UsingFactoryMethod((kernel) =>
                    {
                        lock (_tenantDbs)
                        {
                            var currentUow = kernel.Resolve<ICurrentUnitOfWorkProvider>().Current;
                            var abpSession = kernel.Resolve<IAbpSession>();

                            var tenantId = currentUow != null ? currentUow.GetTenantId() : abpSession.TenantId;

                            if (tenantId == null || tenantId == 1) //host and default tenant are stored in host db
                            {
                                return _hostDb;
                            }

                            if (!_tenantDbs.ContainsKey(tenantId.Value))
                            {
                                _tenantDbs[tenantId.Value] = DbConnectionFactory.CreateTransient();
                            }

                            return _tenantDbs[tenantId.Value];
                        }
                    }, true)
                    .LifestyleTransient()
                );
        }

        #region UsingDbContext

        protected IDisposable UsingTenantId(int? tenantId)
        {
            var previousTenantId = AbpSession.TenantId;
            AbpSession.TenantId = tenantId;
            return new DisposeAction(() => AbpSession.TenantId = previousTenantId);
        }

        protected void UsingDbContext(Action<MyProjectDbContext> action)
        {
            UsingDbContext(AbpSession.TenantId, action);
        }

        protected Task UsingDbContextAsync(Action<MyProjectDbContext> action)
        {
            return UsingDbContextAsync(AbpSession.TenantId, action);
        }

        protected T UsingDbContext<T>(Func<MyProjectDbContext, T> func)
        {
            return UsingDbContext(AbpSession.TenantId, func);
        }

        protected Task<T> UsingDbContextAsync<T>(Func<MyProjectDbContext, Task<T>> func)
        {
            return UsingDbContextAsync(AbpSession.TenantId, func);
        }

        protected void UsingDbContext(int? tenantId, Action<MyProjectDbContext> action)
        {
            using (UsingTenantId(tenantId))
            {
                using (var context = LocalIocManager.Resolve<MyProjectDbContext>())
                {
                    context.DisableAllFilters();
                    action(context);
                    context.SaveChanges();
                }
            }
        }

        protected async Task UsingDbContextAsync(int? tenantId, Action<MyProjectDbContext> action)
        {
            using (UsingTenantId(tenantId))
            {
                using (var context = LocalIocManager.Resolve<MyProjectDbContext>())
                {
                    context.DisableAllFilters();
                    action(context);
                    await context.SaveChangesAsync();
                }
            }
        }

        protected T UsingDbContext<T>(int? tenantId, Func<MyProjectDbContext, T> func)
        {
            T result;

            using (UsingTenantId(tenantId))
            {
                using (var context = LocalIocManager.Resolve<MyProjectDbContext>())
                {
                    context.DisableAllFilters();
                    result = func(context);
                    context.SaveChanges();
                }
            }

            return result;
        }

        protected async Task<T> UsingDbContextAsync<T>(int? tenantId, Func<MyProjectDbContext, Task<T>> func)
        {
            T result;

            using (UsingTenantId(tenantId))
            {
                using (var context = LocalIocManager.Resolve<MyProjectDbContext>())
                {
                    context.DisableAllFilters();
                    result = await func(context);
                    await context.SaveChangesAsync();
                }
            }

            return result;
        }

        #endregion

        #region Login

        protected void LoginAsHostAdmin()
        {
            LoginAsHost(User.AdminUserName);
        }

        protected void LoginAsDefaultTenantAdmin()
        {
            LoginAsTenant(Tenant.DefaultTenantName, User.AdminUserName);
        }

        protected void LoginAsHost(string userName)
        {
            Resolve<IMultiTenancyConfig>().IsEnabled = true;

            AbpSession.TenantId = null;

            var user =
                UsingDbContext(
                    context =>
                        context.Users.FirstOrDefault(u => u.TenantId == AbpSession.TenantId && u.UserName == userName));
            if (user == null)
            {
                throw new Exception("There is no user: " + userName + " for host.");
            }

            AbpSession.UserId = user.Id;
        }

        protected void LoginAsTenant(string tenancyName, string userName)
        {
            var tenant = UsingDbContext(context => context.Tenants.FirstOrDefault(t => t.TenancyName == tenancyName));
            if (tenant == null)
            {
                throw new Exception("There is no tenant: " + tenancyName);
            }

            AbpSession.TenantId = tenant.Id;

            var user =
                UsingDbContext(
                    context =>
                        context.Users.FirstOrDefault(u => u.TenantId == AbpSession.TenantId && u.UserName == userName));
            if (user == null)
            {
                throw new Exception("There is no user: " + userName + " for tenant: " + tenancyName);
            }

            AbpSession.UserId = user.Id;
        }

        #endregion
        
        /// <summary>
        /// Gets current user if <see cref="IAbpSession.UserId"/> is not null.
        /// Throws exception if it's null.
        /// </summary>
        protected async Task<User> GetCurrentUserAsync()
        {
            var userId = AbpSession.GetUserId();
            return await UsingDbContext(context => context.Users.SingleAsync(u => u.Id == userId));
        }

        /// <summary>
        /// Gets current tenant if <see cref="IAbpSession.TenantId"/> is not null.
        /// Throws exception if there is no current tenant.
        /// </summary>
        protected async Task<Tenant> GetCurrentTenantAsync()
        {
            var tenantId = AbpSession.GetTenantId();
            return await UsingDbContext(context => context.Tenants.SingleAsync(t => t.Id == tenantId));
        }
    }
}

這邊其實只有在建構函式加上我們的 PlayerAndMapBuilder

UsingDbContext(context =>
{
    new PlayerAndMapBuilder(context).Create();
});

這樣就會在建立測試用的記憶體資料庫中新增我們的SeedData

接著我們先建立Player資料夾,在裡面新增PlayerAppService_Tests.cs

然後我們來開始在裡面寫測試方法

using MyCompany.MyProject.PlayerApp;
using MyCompany.MyProject.PlayerApp.Dto;
using Shouldly;
using System.Linq;
using Xunit;

namespace MyCompany.MyProject.Tests.Player
{
    public class PlayerAppService_Tests : MyProjectTestBase
    {
        private readonly IPlayerAppService _playerAppService;

        public PlayerAppService_Tests()
        {
            _playerAppService = Resolve<IPlayerAppService>();
        }

        [Fact]
        public void CreatePlayer_Test()
        {
            //Act
            _playerAppService.CreatePlayer(
                new PlayerInput
                {
                    PlayerName = "jakeuj00",
                });

            UsingDbContext(context =>
            {
                var jakeujPlayer = context.Players.FirstOrDefault(u => u.PlayerName == "jakeuj00");

                //Assert
                jakeujPlayer.ShouldNotBeNull();
            });
        }
    }
}

[Fact] 讓 xUnit 知道 void CreatePlayer_Test() 是一個測試方法

Act 新增了一筆資料到Player

Assert 檢查資料是否正確寫入資料庫

 

AAA (排列、作用、判斷提示) 模式是為受測方法撰寫單元測試的常見方式
  • Arrange: [排列] 區段會初始化物件,並為傳遞至受測方法的資料設定值。
  • Act: [作用] 區段會叫用含有所排列參數的受測方法
  • Assert: [判斷提示] 區段會驗證受測方法的動作是否如預期。

這樣我們的測試程式就準備好了,接著我們先編譯一下方案

然後會發現測試方法左上角有測試按鈕可以點進去執行測試

測試成功會打勾

我們把輸入資料庫的資料改成01,測試是否會如期檢測出錯誤

結果確實就過不了檢查

或是你可以打開測試總管 (測試→視窗→測試總管)

來測試全局的方法

到這邊測試單元到一個段落,全篇新手心得也先告一個段落


下一篇

ABP (ASP.NET Boilerplate) 應用程式開發框架 新手教學 No.9 全篇後記

參照

一步一步使用ABP框架搭建正式項目系列教程

PS5