BDD實戰篇 - .NET Core里跑Specflow - 可以跑集成測試和單元測試


這是< 如何用ABP框架快速完成項目 >系列中和 DevOps系列文章其中一篇文章。
 
 
上一篇文章說了如何在.NET Core里安裝Specflow. 然而文章成果只到了hello world級別。
要想真的和實際業務結合,比如要能夠IOC new class實例和能夠調用數據庫和第三方服務。用專業術語來說,就是能跑集成測試和單元測試。這就是這篇文章的目的了。
 
和.NET不一樣的是,.NET Core取消了App.config,並且整個機制都改變了。導致很多.NETer轉.NET Core的時候一臉懵逼
 
所以在.NET Core下運用BDD/TDD也不一樣了。這篇文章就講講如何配置。
 
  1. 原來app.config一分為二,specflow部分划為specflow.json,在 這篇文章里有提到。其他划為appsetting.json
  2. 文件格式也從原來的xml文檔變為json文檔。
  3. 同時還需要把appsetting.json文件屬性設置為如下
  4. 讀取配置的方式也從原來的system.configmanager.appsetting變為 IConfigurationRoot[key]方式。示例代碼如下:
     
        public class AppEnvConfiguration : ITransientDependency
        {
            private readonly IConfigurationRoot _appConfiguration;        
    
            public bool IsEnableADFS => bool.Parse(_appConfiguration["ExternalAuth:WsFederation:IsEnabled"]);
    
            public AppEnvConfiguration(IAppConfigurationAccessor configurationAccessor)
            {
                _appConfiguration = configurationAccessor.Configuration;
            }
        }
    

      

 
Q&A:
  1. 為啥集成測試里面數據庫返回結果數目總是0?
    答:檢查一下測試項目目錄\bin\Debug\netcoreapp2.1目錄下有沒有appsetting.json這個文件,沒有則看看有沒有做上面的第3步. 或者用如下代碼去檢查能否正常讀取到數據庫ConnectionString
    string conString = Microsoft
       .Extensions
       .Configuration
       .ConfigurationExtensions
       .GetConnectionString(this.Configuration, GHITAssetNgConsts.ConnectionStringName);
    

      如果還是解決不了,則先閱讀這篇文章這篇文章
    PS:有人反映上面兩篇文章全英文,看不懂!好吧,我就講重點吧。

    ABP的Test項目默認是只支持mock和EFInMemoery的,不支持集成測試所需要的真實數據庫連接,所以我們要么就搞雙兼容,要么就另開一個測試project單獨去做集成測試。雙兼容關鍵代碼如下:

    public class XXXTestModule : AbpModule
        {
            private readonly IConfigurationRoot _appConfiguration;
    
            bool 使用UAT真實數據庫而不是Mock或者EFInMemory = true;
            public XXXTestModule(XXXEntityFrameworkModule abpProjectNameEntityFrameworkModule)
            {
                if (使用UAT真實數據庫而不是Mock或者EFInMemory)
                {
                    abpProjectNameEntityFrameworkModule.SkipDbSeed = true;
    
                    _appConfiguration = new ConfigurationBuilder()
                        .AddJsonFile("appsettings.json")
                        .AddEnvironmentVariables()
                        .Build();
                }
                else
                {
                    abpProjectNameEntityFrameworkModule.SkipDbContextRegistration = true;
                    abpProjectNameEntityFrameworkModule.SkipDbSeed = true;
                }
            }
    
            public override void PreInitialize()
            {
                Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
                Configuration.UnitOfWork.IsTransactional = false;
    
                // Disable static mapper usage since it breaks unit tests (see https://github.com/aspnetboilerplate/aspnetboilerplate/issues/2052)
                Configuration.Modules.AbpAutoMapper().UseStaticMapper = false;
    
                Configuration.BackgroundJobs.IsJobExecutionEnabled = false;
    
                // Use database for language management
                Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();
    
                if (使用UAT真實數據庫而不是Mock或者EFInMemory)
                {
                    if (Configuration == null) throw new NullReferenceException($"{nameof(Configuration)} is null");
                    Configuration.DefaultNameOrConnectionString = _appConfiguration.GetConnectionString(
        GHITAssetNgConsts.ConnectionStringName
    );
                }
                else
                {
                    RegisterFakeService<AbpZeroDbMigrator<XXXDbContext>>();
                }
    
                Configuration.ReplaceService<IEmailSender, NullEmailSender>(DependencyLifeStyle.Transient);
            }
    

      

    public abstract class XXXNgTestBase : AbpIntegratedTestBase<XXXTestModule>
        {
            void NormalizeDbContext(XXXDbContext context)
            {
                context.EntityChangeEventHelper = NullEntityChangeEventHelper.Instance;
                context.EventBus = NullEventBus.Instance;
                context.SuppressAutoSetTenantId = true;
            }
    
            protected XXXNgTestBase()
            {
                var 使用UAT真實數據庫而不是Mock或者EFInMemory = true;
    
                if (使用UAT真實數據庫而不是Mock或者EFInMemory)
                {
    
                }
                else
                {
                    // Seed initial data for host
                    AbpSession.TenantId = null;
                    UsingDbContext(context =>
                    {
                        NormalizeDbContext(context);
                        new InitialHostDbBuilder(context).Create();
                        new DefaultTenantBuilder(context).Create();
                    });
    
                    // Seed initial data for default tenant
                    AbpSession.TenantId = 1;
                    UsingDbContext(context =>
                    {
                        NormalizeDbContext(context);
                        new TenantRoleAndUserBuilder(context, 1).Create();
                    });
    
                    LoginAsDefaultTenantAdmin();
                }
            }
    

      


    至於為啥不是像ABP官網所說的去配置呢?因為ABP官網說的這種配置方法不是用於集成測試的,而是用於單元測試的。

 
通過以上步驟,終於可以調用數據庫和第三方服務來跑集成測試了。然而還有一個很大問題,單元測試跑不過啊!!!
 
為什么會跑不過呢?
因為BDD和TDD不一樣。
TDD的Test case是可以直接繼承ABP里面的XXXTestBase基類的,這個基類里面提供了IOC new class實例等一系列很有用的方法。
但是BDD的Specflow卻是Test Case和Step Definition分離的。在Step Definition里面是無法調用XXXTestBase基類里的這些方法的。
 
那怎么辦呢?
照如下步驟辦即可:
  1. 新增一個TestBaseWrappers文件夾
  2. 然后根據實際業務從最小化角度來建立TestBaseWrapper類
  3. 在這個類里面寫IOC代碼
  4. 然后再Step Definition類里調用這些TestBaseWrapper類。
 
Q&A:
  1. 為什么不用Step Definition類直接繼承ABP里面的XXXTestBase基類呢?
    答:首先會報錯。然后我在這篇文章里面說到, 我們只有一個Step Definition類,然后分布在多個文件,通過Partial關鍵字來組合。所以如果Step Definition這么大的一個類來直接繼承TestBase基類,然后在TestBase基類里構造函數來IOC初始化所有要調用Service類實例,第一會遇到性能問題,第二會遇到循環調用問題,這畫面太美不敢看啊。
  2. 報錯:Message: System.InvalidOperationException : Mapper not initialized. Call Initialize with appropriate configuration. If you are trying to use mapper instances through a container or otherwise, make sure you do not have any calls to the static Mapper.Map methods, and if you're using ProjectTo or UseAsDataSource extension methods, make sure you pass in the appropriate IConfigurationProvider instance.如何解決?
    所以這道題是我面試必選題之一!!凡是在ABP項目里面使用MapTo擴展方法而不是使用IObjectMapper的,絕對是沒寫過單元測試的!這樣子一下就可以判斷出面試者有沒有寫過單元測試了!!!
  3. 為啥你講了這么多測試方面的知識,開發的知識卻很少?
    答:因為:
    1. 無論是TDD還是BDD,都是測試驅動,先寫測試代碼然后再寫業務開發代碼
    2. 講ABP開發的文章太多,不缺我一個,然而講用ABP去做BDD/TDD的文章卻很少,很需要我去補充
    3. 我現在自己創業,自負盈虧,不像很多開發人員,每月固定有工資,旱澇保收,可以放心的去空談理論。所以我一切以出活為主,以交付實際成果為第一目標,而不是以理論和空談為目標。BDD/TDD可以避免把寶貴的時間投入到項目實際上不需要的理論方面,可以保證我做的東西是客戶所想要的。所以我強烈推薦BDD這個核武器。
    4. 在上一點里面我說出了核心,如何保證你所做的東西就是客戶想要的?這就是BDD與TDD相比,BDD最大的優點啦!!!畢竟,絕大多數情況下,業務人員會比開發人員更了解業務!
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM