美文网首页TDDC#TDD(测试驱动开发)
[Cucumber] TRUNCATE Table Test D

[Cucumber] TRUNCATE Table Test D

作者: 就是91 | 来源:发表于2017-02-14 15:14 被阅读36次

    有效地重構測試程式,可以讓 TDD 或撰寫測試程式的生產力提昇數倍。

    本文介紹當使用 specflow 在進行整合測試或驗收測試時,在 feature 檔案上透過 tag 的標示,即可在 scenario 開始之前,以及 feature 結束之後,清除相關 table 的測試資料,以確保自動測試可重複執行無誤。

    http://specflow.org/

    實務上常透過 specflow 搭配 Entity Framework 等 ORM 框架來進行資料面整合測試的初始化資料、清除資料與查詢驗證資料。範例如下所示:

    Feature 檔案內容

    Feature: BookService
    
    Scenario: Add the first book 
        Given a book for registering
        | ISBN          | Name  |
        | 9789869094481 | specification by example |    
        When Create
        Then Book table should exist a record
        | ISBN          | Name                     |
        | 9789869094481 | specification by example |
    

    Step Definitions 內容

        [Binding]
        public class BookServiceSteps
        {
            private BookService _bookService;
    
            [BeforeScenario()]
            public void BeforeScenario()
            {
                this._bookService = new BookService();
            }
    
            [Given(@"a book for registering")]
            public void GivenABookForRegistering(Table table)
            {
                var bookViewModel = table.CreateInstance<BookViewModel>();
                ScenarioContext.Current.Set<BookViewModel>(bookViewModel);
            }
    
            [When(@"Create")]
            public void WhenCreate()
            {
                var bookViewModel = ScenarioContext.Current.Get<BookViewModel>();
                this._bookService.Create(bookViewModel);
            }
    
            [Then(@"Book table should exist a record")]
            public void ThenBookTableShouldExistARecord(Table table)
            {
                using (var dbcontext = new NorthwindEntitiesForTest())
                {
                    var book = dbcontext.Books.FirstOrDefault();
                    Assert.IsNotNull(book);
    
                    table.CompareToInstance(book);
                }
            }
        }
    

    Production Code 內容

        internal class BookViewModel
        {
            public string ISBN { get; set; }
            public string Name { get; set; }
        }
    
        internal class BookService
        {
            public void Create(BookViewModel bookViewModel)
            {
                var book = new Book { ISBN = bookViewModel.ISBN, Name = bookViewModel.Name };
    
                //production and testing project shouldn't use the same connection string
                //it is just for sample code
                using (var dbcontext = new NorthwindEntitiesForTest())
                {
                    dbcontext.Books.Add(book);
                    dbcontext.SaveChanges();
                }
            }
        }
    

    第一次測試結果,通過測試

    第一次執行測試,順利通過

    測試第二次,測試失敗,duplicate key

    重複執行第二次,測試失敗,新增資料時 pkey 重複

    第二次測試會失敗,如測試報告中所描述,因為 ISBN 是 PKey,不可重複。這是在整合測試很常會碰到的問題,因此我們需要在這類測試案例執行前後,清理 Book 表的資料。

    使用 Entity Framework 清理測試資料

    BeforeScenario 中,加入 Truncate 的指令,程式碼如下:

            [BeforeScenario()]
            public void BeforeScenario()
            {
                this._bookService = new BookService();
                using (var dbcontext = new NorthwindEntitiesForTest())
                {
                    dbcontext.Database.ExecuteSqlCommand("TRUNCATE TABLE [Books]");
    
                    dbcontext.SaveChangesAsync();
                }
            }
    

    如此一來就能確保,每次測試案例執行前,都會將 Books 資料表中的資料清掉。

    但目前作法會碰到兩個問題:

    1. 放在 BeforeScenario 裡面,不需要清理資料的 Scenario 會無謂的執行 TRUNCATE 命令,耗費效能。
    2. 要清理不同的 table 中的測試資料時,能不能有彈性的重用這段程式碼。

    針對這兩個問題,我們可以透過使用 tag 標記來特定的 scenario 要清理哪些 table 來解決。

    使用 Tag 來綁定要實現的邏輯

    首先先把 CleanTable() 從 step definitions 中提煉出來,放到一個共用的 Hooks.cs 中。

    [Binding]
    public sealed class Hooks
    {
        [BeforeScenario()]
        public void CleanTable()
        {
            using (var dbcontext = new NorthwindEntitiesForTest())
            {
                dbcontext.Database.ExecuteSqlCommand("TRUNCATE TABLE [Books]");
    
                dbcontext.SaveChangesAsync();
            }
        }
    }
    

    原本的 step definitions 就不需要再寫任何清 table 的指令。

            [BeforeScenario()]
            public void BeforeScenario()
            {
                this._bookService = new BookService();            
            }
    

    接下來,希望透過 tag 標記與 tag 名稱來決定,這個 scenario 該先清除什麼表的測試資料,把 Hooks 的 CleanTable() 加工一下。

        [BeforeScenario()]
        public void CleanTable()
        {
            var tags = ScenarioContext.Current.ScenarioInfo.Tags
                .Where(x => x.StartsWith("Clean"))
                .Select(x => x.Replace("Clean", ""));
    
            if (!tags.Any())
            {
                return;
            }
    
            using (var dbcontext = new NorthwindEntitiesForTest())
            {
                foreach (var tag in tags)
                {
                    dbcontext.Database.ExecuteSqlCommand($"TRUNCATE TABLE [{tag}]");
                }
    
                dbcontext.SaveChangesAsync();
            }
        }
    

    使用 ScenarioContext.Current.ScenarioInfo.Tags 取得所有 tag 後,篩選出 Clean 開頭的 tag 並 parse table 名稱,執行 Truncate 命令。

    接下來只需要在 scenario 上標記 tag,例如 @CleanBooks 就可以在 scenario 執行前清掉 table [Books] 的資料。

    使用 tag 標記要清除的測試資料表

    同理,需要設定 web UI testing browser 時,也可以用同樣的方式處理,例如:

            [BeforeFeature()]
            [Scope(Tag = "web")]
            public static void SetBrowser()
            {
                SeleniumWebDriver.Bootstrap(
                   SeleniumWebDriver.Browser.Chrome
               );
            }
    

    只要有標記 @web 就會設定 browser 為 Chrome,當然,也可以透過多個 tag 來設定不同的 browser 種類。

    結論

    就像 ASP.NET MVC 的 ActionFilter 一樣,透過 tag 做 Hook 除了讓 feature 上有哪些背景作業要處理看起來可以一目了然,也可以讓共用的程式碼獨立維護、重複使用。

    常見的應用,還有某些功能需要先登入後才能使用,除了在 feature 上用 Background 的方式標記外,也可以透過 tag 來處理。

    開發過程內容,請參考 github repository: CleanTableByTag

    相关文章

      网友评论

        本文标题:[Cucumber] TRUNCATE Table Test D

        本文链接:https://www.haomeiwen.com/subject/zoixwttx.html