天天看點

網站測試自動化系統—在測試代碼中寫死測試資料

之是以要這樣做(按照程式設計的術語講是寫死),是因為按照等價類劃分,固定的測試資料一般都已經被其他測試用例覆寫了。請考慮下面這個例子,假設你要測試一個部落格網站(例如部落格園)的文章評論功能,例如測試禁用一篇文章的評論功能,或者是測試文章作者删除評論的功能。按照正常的流程,肯定是需要先編碼釋出一篇文章,然後再編碼指定的評論功能測試用例。這樣的流程有以下幾個缺點:

1.         需要備援的編碼,因為每個評論測試用例的代碼都要包含釋出文章的步驟,在程式設計裡面,我們都是極力推薦,什麼隻要代碼在不同的地方重複兩次,就要考慮是否将它封裝成一個函數之類的理念。這種包含備援編碼的方式是我們在測試過程中極力要避免地,否則,程式員可能哪天心情很好,重構一下代碼,破壞了一些網頁的HTML結構—但是從使用者的角度來看又沒有任何差別;這種代碼重構,作為測試人員隻能跟着程式員的代碼重構,修改測試代碼,那個時候,你當然會希望改的地方越少越好啦。

1)        一個測試用例可以獨自執行成功,就是說如果是單獨執行這一個測試用例的話,這個測試用例是可以執行成功的—否則就是産品編碼的失誤(Bug)。舉個例子,你正要編碼測試一個管理部落格文章的功能,這個功能通常來說都是登入使用者才可以使用的。然而,也許你剛剛編碼完畢一個登入方面的測試用例,而且用例執行完畢的時候,沒有執行登出操作。這個時候你不能想當然地以為下一個測試用例一定就是你現在正在編碼的文章管理的測試用例。

因為測試人員既保留有将多個測試用例任意排列執行的權力,也可以選擇單獨執行這一個測試用例—比如程式員剛剛重構了文章管理功能的代碼,為了節省測試時間,測試人員可能會選擇隻執行文章管理方面的測試用例。是以不要将自己的命運寄托在别人手裡。即除了整個團隊都公認的前提以外,不要相信任何前提。

2)        測試用例可以在任意排列的用例序列中執行通過,是以測試代碼應該盡量保護測試環境。舉個例子,你設計了一個管理使用者權限的測試用例,一般來說這種功能隻有管理者才有權限操作的。然而,也許另一個粗心大意的測試工程師編碼了一個測試删除使用者的用例,恰好将管理者删除了,而你的用例正好在他的用例之後執行……己所不欲,勿施于人,既然你不希望碰到這種情況,那麼在編碼自己的測試用例之前也應該避免類似的事情發生。

回過頭來再舉評論管理測試用例的設計,于是你的幾個測試代碼可能看起來像下面這樣:

[TestMethod]

public void BlogCommentIsDisabled()

{

    TestLibrary.UserHelper.LogOnAsAdmin();

    var blog = TestLibrary.BlogHelper.CreateBlog("部落格文章标題", "文章内容");

    // 去管理文章的網頁

    TestLibrary.BlogHelper.ManageArticles();

    // 在文章管理的網頁的文章清單裡依次查找标題為

    // "部落格文章标題"的文章連接配接,

    var blogListItem = TestLibrary.BlogHelper.FindBlog(blog.Title);

    // 并且在網頁上點選"浏覽" 這個連結,打開閱讀文章的網頁

    blogListItem.View();

    // 評論這篇文章

    TestLibrary.BlogHelper.Comment(blog);

    // 然後執行一些驗證判斷評論功能的确被禁用掉了

    // ...

}

public void DeleteBlogComment()

    var comment = TestLibrary.BlogHelper.Comment(blog);

    // 找到剛才的評論、删除評論,然後執行驗證确定

    // 評論被删除掉

每個測試用例單獨執行的時候,都不會有任何問題,但是兩個放在一起執行的時候,問題就來了,兩個用例建立了同名的文章,這樣就直接導緻測試結果的不穩定。為了解決這個問題,也許有人會建立一個随機生成文章标題的幫助類(Helper Class),這種編碼的難度很大,因為需要確定文章的标題永遠是唯一的(或許可以考慮Guid?)。

2.         節省測試的時間,在用例中執行過多的步驟也會增加測試時間。雖然測試團隊都會在晚上批量執行自動化測試用例,但是在産品開發的過程當中,測試用例通過率不能達到100%是很正常的。對于每一個失敗的測試用例,測試人員都要分析失敗的原因—判斷是産品的缺陷導緻的,還是由于測試代碼本身的問題引起的。額外的測試步驟也會相應地增加測試人員分析失敗的時間(一般測試人員都會重新執行一遍測試代碼來找出問題原因)。

3.         增加不必要的測試用例失敗,測試可以分好幾塊,一種是功能測試,也就是驗證産品的功能是否可以正常工作;一種是壓力測試,即測試産品在極端情況下的執行情況;還有其他的例如性能測試,國際化測試等等。一般來說,不同的測試都會有自己的自動化測試用例集合。如果在功能測試當中,用例代碼在系統裡面添加了很多備援資料,執行的測試用例多了,必然導緻網站的性能和反應速度會有所下降。而在測試代碼中,一般都會在執行一步操作以後,等待一段時間—等網頁的内容重新整理。網站反應速度的下降,直接導緻測試失敗。例如本來在編寫測試代碼的時候,3秒鐘肯定會重新整理的網頁,在測試執行的環境中,因為過多的備援資料,30秒可能都打不開一個網頁。當然啦,網站反應速度的下降肯定是産品代碼的缺陷,但是不應該将壓力測試和功能測試混合起來做。

是以,我個人建議,在測試過程中,例如前面舉的評論功能的測試中,完全可以事先在網站的資料庫中先建立好一篇或多篇專門用來做評論測試的文章。而每天晚上,在大規模執行自動化測試用例之前,編寫一個小的腳本,将網站的資料庫替換成這個基準資料庫。

又比如,為了測試使用者權限管理的功能,完全可以事先在網站的資料庫當中先準備好一個管理者帳号,這個管理者帳号和密碼可以當作一個常量,然後測試代碼裡都使用這個帳号來執行權限管理的測試。例如下面的代碼:

public class Consts

    public const string TimeToWaitForPageToLoad = "30000";

    public const string AdminUserName = "administrator";         

    public const string AdminPassword = "0123456";

}         

public class UserHelper : UIHelperBase

    public UserHelper(TestLibrary settings)

        : base(settings)

    {

    }

    public void LogOnAsAdmin()

        LogOn(TestLibrary.Consts.AdminUserName, TestLibrary.Consts.AdminPassword);

    public void LogOn(string username, string password)

        if (String.IsNullOrEmpty(username))

            throw new CaseErrorException(new ArgumentNullException("username"));

        if (String.IsNullOrEmpty(password))

            throw new CaseErrorException(new ArgumentNullException("password"));

        selenium.Open("/");

        Thread.Sleep(2000);

        if (selenium.IsElementPresent("link=Log On"))

        {

            selenium.Click("link=Log On");

        }

        if (selenium.IsElementPresent("link=Login"))

            selenium.Click("link=Login");

        selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad);

        selenium.Type("username", username);

        selenium.Type("password", password);

        selenium.Click("//input[@value='Log On']");

在上面的代碼中,我也把等待網頁重新整理的時間設定成常量。對于在測試代碼中使用事先在基準資料庫中準備的測試資料,需要一點程式設計技巧。請先看下面的代碼,下面的代碼是一段記錄通過網頁操作建立文章的代碼:

public class Blog : UIHelperBase

    // 部落格的标題

    public string Title { get; private set; }

    // 部落格的超連結

    public string Permalink { get; private set; }

    // 部落格的超連結文本

    public string MenuText { get; private set; }

    public string Owner { get; private set; }

    public Blog(TestLibrary settings, string title,

        string permalink, string menutext, string owner)

        Title = title;

        Permalink = permalink;

        MenuText = menutext;

        Owner = owner;

    // 通過網頁界面的操作建立一篇新文章

    //

    // PostSetting是一個結構,包含了一篇新文章的所有元素,

    // 例如文章标題,内容等等.

    public Post CreatePost(PostSettings settings)

        if (settings == null)

            throw new CaseErrorException(new ArgumentNullException("settings"));

        if (!String.IsNullOrEmpty(settings.Body))

            throw new CaseErrorException("Set post body is not implemented yet!");

        if (settings.PublishDateTime.HasValue)

            throw new CaseErrorException("PublishDateTime is not implemented yet!");

        // selenium這個變量,你可以想象成是一個正在浏覽網頁的網友的封裝

        selenium.Click("link=Admin");

        selenium.Click("link=Manage Blogs");

        selenium.WaitForPageToLoad("60000");

        selenium.Click(String.Format("link={0}", Title));

        selenium.Click("link=New Post");

        selenium.Type("Routable_Title", settings.Title);

        selenium.Type("Tags", settings.Tags);

        if (settings.Permalink != null)

            selenium.Type("Routable_Slug", settings.Permalink);

        if (settings.DisableNewComments)

            selenium.Click("CommentsActive");

        if (settings.PublishSetting == PostSettings.PublishSettings.PublishNow)

            selenium.Click("Command_PublishNow");

        else if ( settings.PublishSetting == PostSettings.PublishSettings.PublishLater )

            throw new CaseErrorException("PublishLater is not implemented yet!");

        selenium.Click("submit.Save");

        return new Post(TestSettings, settings, this);

public class PostSettings

    public enum PublishSettings

        SaveDraft,

        PublishNow,

        PublishLater

    public string Title { get; set; }

    public string Permalink { get; set; }

    public string Body { get; set; }

    public string Tags { get; set; }

    public bool DisableNewComments { get; set; }

    public PublishSettings PublishSetting { get; set; }

    public DateTime? PublishDateTime { get; set; }

public class Post : UIHelperBase

// 當初建立文章的原始詳細資訊

    public PostSettings Settings { get; private set; }

    // 文章的标題 – 從網頁上擷取

    public string Title { get { return selenium.Read(...); } }

// 下面省略文章相關的操作若幹

// ...

    public Post(TestLibrary settings, PostSettings postSettings, Blog blog)

        Settings = postSettings;

        ContainerBlog = blog;

從上面的代碼中,你可以觀察到,Post的屬性,除了Settings屬性以外,其他的屬性都是從網頁上直接讀取的—當然是假設目前網頁正在顯示對應的文章。是以,要将基準資料庫內建到自動化測試代碼中來,隻要執行個體化一個PostSettings變量就好了。TestLibrary是 負責連接配接到Selenium-RC,并儲存對應連接配接的類。下面的代碼示範了這個思想:

public class TestLibrary

    public UserHelper UserHelper { get; private set; }

    public BlogHelper BlogHelper { get; private set; }

    public CommentHelper CommentHelper { get; private set; }

    public Blog DefaultBlog { get; private set; }

    public Post DefaultPost { get; private set; }

    public ISelenium Selenium { get; private set; }

    public string SiteUrl { get; private set; }

    public class Consts

        public const string TimeToWaitForPageToLoad = "30000";

        public const string AdminUserName = "administrator";      

        public const string AdminPassword = "0123456";

    public TestLibrary(ISelenium selenium)

        this.UserHelper = new UserHelper(this);

        this.BlogHelper = new BlogHelper(this);

        this.CommentHelper = new CommentHelper(this);

        Selenium = selenium;

        InitialDefaultSiteDate();

    private void InitialDefaultSiteDate()

        DefaultBlog = new Blog(this, "Default Test Blog", "default-test-blog", "Default Test Blog", Consts.AdminUserName);

        DefaultPost = new Post(this, new PostSettings()

            Title = "Default Test Post",

            Permalink = "default-test-post",

            Body = "This is for web site testing purpose.",

            Tags = "Test",

            PublishSetting = PostSettings.PublishSettings.PublishNow

        },

        DefaultBlog);

下面是TestLibrary的完整源代碼:

        public const string AdminUserName = "administrator";         

        public const string ContributorUser = "Contributor1";

        public const string AuthorUser = "Author1";

        public const string ModeratorUser = "Moderator1";

        public const string EditorUser = "Editor1";

        public const string CommonPassword = "0123456";

        public const string DefaultSeleniumHost = "localhost";

        public const int DefaultSeleniumPort = 4444;

        public const string DefaultBrowser = "*firefox";

        public const string DefaultSite = "http://localhost:30320";

    public static TestLibrary SetupTest(TestContext testContext)

        if (testContext != null && testContext.DataRow != null && testContext.DataRow.Table.Columns.Contains("seleniumHost"))

            return SetupTest(testContext.DataRow["seleniumHost"].ToString(),

                Int32.Parse(testContext.DataRow["seleniumPort"].ToString()),

                testContext.DataRow["browser"].ToString(),

                testContext.DataRow["site"].ToString());

        else

            return SetupTest(Consts.DefaultSeleniumHost, Consts.DefaultSeleniumPort,

                Consts.DefaultBrowser, Consts. DefaultSite);

    public static TestLibrary SetupTest(string seleniumHost, int seleniumPort,

        string browser, string site)

        var selenium = new DefaultSelenium(

            seleniumHost, seleniumPort, browser, site);

        selenium.Start();

        return new TestLibrary(selenium) { SiteUrl = site };

    public void Shutdown()

        try

            Selenium.Stop();

        catch (Exception)

            // Ignore errors if unable to close the browser

未完待續……

本文轉自 donjuan 部落格園部落格,原文連結:http://www.cnblogs.com/killmyday/archive/2010/03/19/1690247.html   ,如需轉載請自行聯系原作者