愛流浪的小風

技術隨手寫

[Tool]使用Specflow以及Visual Studio Online,打造可維護的負載測試

| Comments

如果你的網站也是一個線上服務平台的話,該如何知道自己的網站能不能撐住瞬間巨量的衝擊呢?答案就是負載測試(壓力測試),以往我們要進行負載測試最困擾的就是必須準備大量的機器,同時針對待測網站給予大量的流量壓力,才能完成,而在雲端服務盛行的今天,我們只要透過Visual Studio Online的負載測試功能,讓你連一台機器都不用準備,就可以對你在雲端的網站進行負載測試,甚至更能夠直接使用原本為了網站做自動化測試的Specflow腳本來進行,讓你的負載測試不再是一大堆使用錄製的檔案,而是可以輕鬆讀懂而且好維護的測試案例!

建立網站

我們假設我們所擁有的是一個線上購物網站,想要評估在扣除庫存的程式邏輯是否可以在瞬間大流量的時候也運作正常,而且不會有超賣的情況發生,以下分別撰寫兩種方法來,並透過壓力測試來評估哪一種方法可以經得起大流量的衝擊。(分別是直接使用TransactionScope並使用Serialize鎖定庫存並扣除,和使用ReadCommited搭配樂觀鎖定來扣除庫存)

  1. 建立一個新的Mvc網站

  2. 使用Nuget安裝Entity Framework

  3. 建立庫存的資料結構和DbContext,一種是使用Serialize鎖定,一種是使用樂觀鎖定

    public class Product
    {   
        [Key]
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        public int TotalQty { get; set; }
    
        public int RequestedQty { get; set; }        
    }       
    
    public class ProductWithVersion
    {
        [Key]
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        public int TotalQty { get; set; }
    
        public int RequestedQty { get; set; }
    
        [Timestamp]
        public byte[] TimeStamp { get; set; }
    }
    
    public class ProductDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
    
        public DbSet<ProductWithVersion> ProductWithVersions { get; set; }
    }
    
  4. 套件管理主控台執行以下指令,新增一個Migration

    Enable-Migrations
    Add-Migration Init
    

  5. 打開Configuration.cs,修改Seed的內容,給予資料庫初始值

    protected override void Seed(StockRequest.Models.ProductDbContext context)
    {            
        context.Products.AddOrUpdate(
            i => i.Name,
            new Product { Name = "Test", TotalQty = 500, RequestedQty = 0 }
        );
    
        context.ProductWithVersions.AddOrUpdate(
            i => i.Name,
            new ProductWithVersion { Name = "Test", TotalQty = 500, RequestedQty = 0 }
        );
    }   
    
  6. 套件管理主控台執行以下指令,更新資料庫

    Update-Database
    

  7. 新增ProductController,分別提供有鎖定和沒鎖定的購買商品扣除庫存操作頁面

    public class ProductController : Controller
    {
        private ProductDbContext dbContext = new ProductDbContext();
    
        // GET: Stock
        public ActionResult Index(int id)
        {
            var model = this.dbContext.Products
                            .Where(i => i.Id == id)
                            .FirstOrDefault();
    
            return View(model);
        }
    
        [ValidateAntiForgeryToken]
        [HttpPost]
        public ActionResult Index(FormCollection formData)
        {
            var id = int.Parse(formData["Id"]);
    
            using (TransactionScope scope = new TransactionScope())
            {
                var model = this.dbContext.Products
                                    .Where(i => i.Id == id)
                                    .FirstOrDefault();
    
                model.RequestedQty += 1;
                this.dbContext.SaveChanges();
    
                scope.Complete();
            }
    
            return RedirectToAction("Result");
        }
    
        public ActionResult Nolock(int id)
        {
            var model = this.dbContext.ProductWithVersions
                            .Where(i => i.Id == id)
                            .FirstOrDefault();
    
            return View(model);
        }
    
        [ValidateAntiForgeryToken]
        [HttpPost]
        public ActionResult Nolock(FormCollection formData)
        {
            var id = int.Parse(formData["Id"]);
    
            var model = this.dbContext.ProductWithVersions
                                    .Where(i => i.Id == id)
                                    .FirstOrDefault();            
    
            model.RequestedQty += 1;
            this.dbContext.SaveChanges();           
    
            return RedirectToAction("Result");
        }
    
        public ActionResult Result()
        {
            return View();
        }
    }
    
  8. 新增Index.cshtml和NoLock.cshtml

    Index.cshtml

    <h2>Request Qty With Lock</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    
        <div class="form-horizontal">
            <h4>Product @Model.Id,Current Qty: @(Model.TotalQty - Model.RequestedQty)</h4>
            <hr />
    
            @Html.HiddenFor(model => model.Id)        
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Buy" class="btn btn-default" />                
                </div>
            </div>
        </div>
    }
    

    NoLock.cshtml

    <h2>Request Qty Without lock</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    
        <div class="form-horizontal">
            <h4>Product @Model.Id,Current Qty: @(Model.TotalQty - Model.RequestedQty)</h4>
            <hr />
    
            @Html.HiddenFor(model => model.Id)
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Buy" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
    
  9. 完成站台,可成功連上並扣除庫存

參考資料:

撰寫自動化測試

  1. 新增一個單元測試專案

  2. 使用Nuget在專案中安裝以下套件

    Install-Package Specflow.FluentAutomation
    Install-Package FluentAutomation.SeleniumWebDriver
    Install-Package PhantomJS
    
  3. 打開app.config,設定specflow使用mstest執行單元測試

    <specFlow>
        <unitTestProvider name="mstest" />
        <plugins>
            <add name="FluentAutomation" path="plugin" type="Generator" />
        </plugins>
    </specFlow>
    
  4. 新增購買商品.feature

    #language: zh-TW
    功能: 購買商品
        為了提供可以共買商品的頁面
        提供給使用者
        購買想要的商品
    
    場景: 使用會Lock資料的購買商品頁面購買商品
        假設 開啟商品頁 "http://localhost:42505/Product/Index/1"
        當 點選購買按鈕
        那麼 購買成功
    
    場景: 使用不會Lock資料的購買商品頁面購買商品
        假設 開啟商品頁 "http://localhost:42505/Product/Nolock/1"
        當 點選購買按鈕
        那麼 購買成功
    
  5. 新增購買商品步驟.cs

    [Binding]
    public class 購買商品步驟: StepDefinitionBase
    {
        [BeforeFeature]
        public static void StartUp()
        {
            SeleniumWebDriver.Bootstrap(SeleniumWebDriver.Browser.PhantomJs);
        }
    
        [Given(@"開啟商品頁 ""(.*)""")]
        public void 假設開啟商品頁(string url)
        {
            I.Open(url);
        }
    
        [When(@"點選購買按鈕")]
        public void 當點選購買按鈕()
        {
            I.Click(".btn");
        }
    
        [Then(@"購買成功")]
        public void 那麼購買成功()
        {
            I.Assert.Text("Request Qty Success").In("h2");
        }
    }
    
  6. 執行自動化測試,可以看到測試都成功了!

參考資料:

部署網站到Azure

為了讓Visual Studio Online可以進行壓力測試,我們使用Azure來放置我們的網站,當然你也可以使用一個有公開的主機作為測試使用,但必須評估自己的主機是否乘載得住流量,避免產生問題。

  1. 在網頁專案點選滑鼠右鍵,選擇發行,可以看到發行設定視窗,選擇第一個Microsoft Azure Web Apps

  2. 如果已經有Web App的話,可以選擇現有的,如果沒有的話點選新增,並設定Web App所需的資訊

  3. 在設定記得勾選執行Code First移轉

  4. 按下發行,稍待片刻之後,我們就可以在公開網路上看到我們的網站了!

使用Visual Studio Online執行負載測試

注意:剛開始執行負載測試的時候,建議人數和測試的時間不要一下太大量,以免所花費的金額超過預期!Visual Studio Online也很佛心的每月提供20000虛擬使用者分鐘數的單位給大家免費做壓力測試,大家可以多多利用!

  1. 把剛剛自動化測試的網址,改為部署好網站的網址,並在測試專案新增一個負載測試,並按照需求設定條件,在混合測試的地方選擇剛剛寫好的其中一個測試(一次一個,方便比較)

  2. 方案點選滑鼠右鍵,新增一個項目,選擇測試設定

  3. 在設定把測試回合位置切換為Visual Studio Online (記得Visual Studio要先連接VS Online的Team Project)

  4. 因為我們在FluentAutomation為了方便,使用的是Phantom.js,所以在部署的地方也要把Phantomjs.exe加入裡面

  5. 點選剛剛的負載測試,開始連接並設定測試資料

  6. 測試正在進行中

  7. 測試完成之後,可以下載報告回來看

  8. 我們可以依照上面的步驟繼續完成另外一種方法的壓力測試

小結

透過實際的進行壓力測試之後,我們可以輕鬆地找出網站的瓶頸在哪邊,並且加以改善,並且有準確的數據作為網站架構的依據,可以得知自己的服務極限在哪,並在未來做進階的規劃。

而且這些已經寫好的Specflow測試,不但可以使用在自動化測試,還可以直接作為壓力測試的腳本,除了讓測試可以用在多個地方之外,還是使用語意化的方法撰寫測試案例,讓維護起來更加的輕鬆!關於以上的內容,若有任何問題歡迎一起討論喔!

Comments

comments powered by Disqus