前回の記事 データ駆動型テスト では、テスト データとテスト ステップを表すコードを分離する手法について説明しました。テストの観点からは、カバーできるテスト シナリオが多いほど良いことが望まれますが、自動テスト コードを設計および作成する場合、自動テスト コードの作成を簡素化するために、いくつかの固定テスト データを事前に設計できます。
この理由 (プログラミング用語でハードコーディング) は、固定テスト データが通常、等価クラスによる他のテスト ケースですでにカバーされているためです。ブログサイト(ブログガーデンなど)の記事コメント機能をテストしたい場合を想定して、以下の例を考えてください。例えば、記事のコメント機能を無効にする機能をテストしたい場合や、記事のコメント機能をテストしたい場合などです。記事の作成者はコメントを削除してください。通常のプロセスによれば、必ず最初に記事をコーディングして公開し、その後、指定されたコメント機能のテスト ケースをコーディングする必要があります。このようなプロセスには次のような欠点があります。
1. 各コメント テスト ケースのコードには、記事を公開するための手順が含まれている必要があるため、冗長なコーディングが必要です。プログラミングでは、コードが異なる場所で 2 回繰り返される限り、コードを公開するかどうかを考慮する必要があることを強くお勧めします。関数などのアイデアをカプセル化します。冗長なコーディングを組み込むこの方法は、テスト プロセス中に避けようとしているものです。そうしないと、プログラマーがある日機嫌を良くしてコードをリファクタリングし、一部の Web ページの HTML 構造を破壊する可能性があります。ただし、ユーザーの観点からすると、違いはありません。テスターとして、この種のコード リファクタリングを実行できるのは、プログラマのコード リファクタリングに従い、テスト コードを変更することだけです。そのとき、もちろん、変更が少ないほど良いと期待するでしょう。
この欠点について、ブログを作成するテスト ステップが関数に効果的にカプセル化されているのではないか、なぜ冗長性があると言われるのか、と言う人もいるかもしれません。これは、自動テストのプロセスで、テスターが定期的に (一部の高レベルのソフトウェア開発チームは毎日必要とする)、書かれたすべてのテスト コードをバッチで実行するためです。これには、原則として、テスト ケースのコーディングにとって非常に重要な 2 つのことが含まれます。
1) テスト ケースは単独で正常に実行できます。つまり、このテスト ケースを単独で実行すると、テスト ケースは正常に実行できます。そうでない場合は、製品のコーディング エラー (バグ) になります。たとえば、通常はログインしているユーザーのみが利用できるブログ投稿を管理する機能をコーディングしてテストしているとします。ただし、ログイン テスト ケースをコーディングしたばかりで、テスト ケースの実行が終了してもログアウト操作が実行されない可能性があります。現時点では、次のテスト ケースが現在コーディングしている記事管理のテスト ケースでなければならないことを当然のことと考えることはできません。
テスターは複数のテスト ケースを任意に配置して実行する権利を保持するだけでなく、たとえばプログラマが記事管理機能のコードをリファクタリングしたばかりである場合など、このテスト ケースのみを実行することも選択できます。 、テスターは、記事管理のテスト ケースのみを実行するを選択できます。ですから、自分の運命を他人の手に委ねないでください。それは、チーム全体が同意する前提以外の前提を信じないことです。
2) テスト ケースは任意の順序で実行できるため、テスト コードはテスト環境を可能な限り保護する必要があります。たとえば、ユーザー権限を管理するためのテスト ケースを設計したとしますが、一般的に、この機能を操作する権限を持つのは管理者だけです。しかし、おそらく別の不注意なテスト エンジニアがユーザーの削除をテストするユース ケースをコーディングし、それが偶然にも管理者を削除し、あなたのユース ケースは彼のユース ケースの直後に実行された可能性があります... あなたが望むことを他の人にしないでください。このような状況には遭遇したくないので、独自のテスト ケースをコーディングする前に、同様のことが起こらないようにする必要があります。
コメント管理テスト ケースの設計を振り返ると、いくつかのテスト コードは次のようになります。
[試験方法] public void BlogCommentIsDisabled() { TestLibrary.UserHelper.LogOnAsAdmin(); var blog = TestLibrary.BlogHelper.CreateBlog("ブログ投稿のタイトル", "記事の内容"); // 記事を管理するための Web ページに移動します TestLibrary.BlogHelper.ManageArticles(); // 記事管理Webページの記事一覧で、タイトルを検索 // 「ブログ投稿タイトル」の記事リンク、 var blogListItem = TestLibrary.BlogHelper.FindBlog(blog.Title); // そして、Web ページ上の「参照」リンクをクリックして、記事を読むための Web ページを開きます blogListItem.View(); // この投稿にコメントする TestLibrary.BlogHelper.Comment(ブログ); // 次に、コメント機能が実際に無効になっていることを確認するために検証を実行します。 // ... } [試験方法] public void DeleteBlogComment() { TestLibrary.UserHelper.LogOnAsAdmin(); var blog = TestLibrary.BlogHelper.CreateBlog("ブログ投稿のタイトル", "記事の内容"); // 記事を管理するための Web ページに移動します TestLibrary.BlogHelper.ManageArticles(); // 記事管理Webページの記事一覧で、タイトルを検索 // 「ブログ投稿タイトル」の記事リンク、 var blogListItem = TestLibrary.BlogHelper.FindBlog(blog.Title); // そして、Web ページ上の「参照」リンクをクリックして、記事を読むための Web ページを開きます blogListItem.View(); // この投稿にコメントする var コメント = TestLibrary.BlogHelper.Comment(ブログ); // 先ほどのコメントを見つけてコメントを削除し、検証を実行して確認します // コメントは削除されます } |
それぞれのテストケースが別々に実行される場合には問題ありませんが、2 つのテストケースが同時に実行される場合に問題が発生し、2 つのユースケースによって同じ名前のアーティクルが作成され、テスト結果の不安定に直結します。この問題を解決するには、誰かが記事のタイトルをランダムに生成するヘルパー クラス (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']"); selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad); } } |
在上面的代码中,我也把等待网页刷新的时间设置成常量。对于在测试代码中使用事先在基准数据库中准备的测试数据,需要一点编程技巧。请先看下面的代码,下面的代码是一段记录通过网页操作创建文章的代码:
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) : base(settings) { 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.Open("/"); selenium.Click("link=Admin"); selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad); selenium.Click("link=Manage Blogs"); selenium.WaitForPageToLoad("60000"); selenium.Click(String.Format("link={0}", Title)); selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad); selenium.Click("link=New Post"); selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad); 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"); selenium.WaitForPageToLoad(TestLibrary.Consts.TimeToWaitForPageToLoad); 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) : base(settings) { 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 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 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 AdminPassword = "0123456"; public const string DefaultSeleniumHost = "localhost"; public const int DefaultSeleniumPort = 4444; public const string DefaultBrowser = "*firefox"; public const string DefaultSite = "http://localhost:30320"; } 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); } 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 } } } |
【整整200集】超超超详细的Python接口自动化测试进阶教程,真实模拟企业项目实战!!