端到端测试应该是自包含的吗?

软件测试 自动化测试 端到端
2022-02-04 20:06:09

我正在使用 Selenium 自动化 Web UI 测试。这些测试部分是本着单元测试的精神编写的,即他们尽最大努力使应用程序保持与他们发现它时相同的状态。例如,如果测试使用 Web UI 注册支持票证,则预计会删除它并使用 Web UI 结束测试。

与单元测试相比,这些类型的测试做得更多,不是在实际执行的代码或触发的流程方面,而是在测试的操作方面。一种这样的假设测试是:

  1. (Actor 1) 注册支持票;
  2. (Actor 1) 用初始数据发表评论;
  3. (演员 2) 回复要求提供更多信息
  4. (演员一) 上传更多资料
  5. (演员 2)评论问题已修复
  6. (演员 1) 关闭问题

所有这些操作都在测试期间得到验证,如果其中一个操作失败,则应用程序数据不一致。

有没有更好的方法使端到端测试自包含和幂等?

4个回答

我个人认为有几个关键原则。

  1. 在可能的情况下,测试应该假设它们之前已经运行过并且失败了。

  2. 一些测试想要验证一个特性,而另一些则需要在测试其他东西的过程中“通过”一个特性。

  3. 我可以横向扩展并使用多台机器,这样我就可以并行运行很多测试,而且我一次运行没有时间限制。

考虑到这些,我会在可能的情况下:

  • 让每个测试为自己生成不依赖于先前测试通过的唯一数据
  • 将执行逻辑与验证代码分开

因此,对于您的示例,我将为您的每个操作创建并执行六个单独的“对象”:注册支持票证、添加评论、回复更多信息请求、上传附加信息和关闭票证。

然后我会在六个测试用例中将它们串在一起,如下所示:

  1. 注册支持票并验证
  2. 注册支持票,然后添加评论并验证
  3. 注册支持票,然后添加评论,回复请求以获取更多信息并验证
  4. 注册支持票,然后添加评论,回复请求以获取更多信息,上传更多信息并验证
  5. 注册支持票,然后添加评论,回复请求以获取更多信息,上传更多信息评论问题已修复验证
  6. 注册支持票,然后添加评论,回复请求以获取更多信息,上传更多信息评论,问题已修复,关闭并验证

当我测试注册 6 次时,我会构建一个测试数据对象,并且每次使用不同的测试数据,这样我每次注册时也可以测试不同的场景。

所以主要的“成本”是执行时间,因为我执行 6 个长测试而不是一个(但使用开源工具意味着我可以轻松扩展)对象的使用意味着我没有到处都有大量重复的代码。这也意味着如果应用程序的注册票部分发生更改,我只在一个地方更改一个对象,所有测试都会继续运行。

这是使用“ Parkcalc ”的单个“对象”的代码中的这种方法

    [TestMethod]
    public void EconomyLessThanOneHour()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "10:59", "AM", "today");
    }

    [TestMethod]
    public void EconomyExactlyOneHour()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "11:00", "AM", "today");
    }

    [TestMethod]
    public void EconomyMoreThanOneHour()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "11:01", "AM", "today");
    }

    [TestMethod]
    public void EconomyJustLessThanOneDay()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "9:59", "AM", "today+1");
    }

    public void EconomyExactlyOneDay()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "10:00", "AM", "today+1");
    }

    [TestMethod]
    public void EconomyJustMoreThanOneDay()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "10:01", "AM", "today+1");
    }

    [TestMethod]
    public void EconomyJustLessThanOneWeek()
    {
        Parking.CalculateAndVerify(ParkingType.EconomyParking, "10:00", "AM", "today", "9:59", "AM", "today+7");
    }

以及CalculateAndVerify的实现......

 namespace Parkcalc.Logical
 {
    public class Parking
    {
        public static void OpenHome()
        {
             Physical.NavigateTo.Homepage();
        }

         public static void Calculate(ParkingType parkingType, string inTime, string inAMPM, string inDate, string outTime, string outAMPM, string outDate)
         {
            inDate = Utility.CalculateDate(inDate);
            outDate = Utility.CalculateDate(outDate);
            Physical.Parking.Calculate(parkingType, inTime, inAMPM, inDate, outTime, outAMPM, outDate);
         }

         public static void CalculateAndVerify(ParkingType parkingType, string inTime, string inAMPM, string inDate, string outTime, string outAMPM, string outDate)
         {
             Calculate(parkingType, inTime, inAMPM, inDate, outTime, outAMPM, outDate);
             Verification.Verify.VerifyResult(parkingType, inTime, inAMPM, inDate, outTime, outAMPM, outDate);
         }
     }
 }

和下一层

namespace Parkcalc.Physical
{
    public static class Parking
    {

        public static void Calculate(ParkingType parkingType, string inTime, string inAMPM, string inDate, string outTime, string outAMPM, string outDate)
        {
            NavigateTo.Homepage();
            Browser.SetValue(Controls.ParkingCalculator.ddlLot,EnumValues.GetParkingType(parkingType));
            if (!string.IsNullOrEmpty(inTime))
            {
                Browser.SetValue(Controls.ParkingCalculator.txtEntryTime, inTime);
            }

            if (!string.IsNullOrEmpty(inAMPM))
            {
                Browser.SetValue(Controls.ParkingCalculator.rdoEntryTimeAMPM, inAMPM);
            }

            if (!string.IsNullOrEmpty(inDate))
            {
                Browser.SetValue(Controls.ParkingCalculator.txtEntryDate, inDate);
            }

            if (!string.IsNullOrEmpty(outTime))
            {
                Browser.SetValue(Controls.ParkingCalculator.txtExitTime, outTime);
            }

            if (!string.IsNullOrEmpty(outAMPM))
            {
                Browser.SetValue(Controls.ParkingCalculator.rdoExitTimeAMPM, outAMPM);
            }

            if (!string.IsNullOrEmpty(outDate))
            {
                Browser.SetValue(Controls.ParkingCalculator.txtExitDate, outDate);
            }

            Browser.Invoke(Controls.ParkingCalculator.btnCalculate);
        }

        public static string GetResult()
        {
           return Browser.GetValue(Controls.ParkingCalculator.txtResult);
        }
    }
}

我在这里有一个使用 WatiN 的完整 C# 示例... http://testingstax.codeplex.com/SourceControl/changeset/view/7938#

对于测试数据,我通常会构建一个数据生成器并添加唯一的随机字符串,这样就不会发生数据冲突。

我见过以这种方式编写的测试,但是如果您的测试套件依赖于每个测试总是在自己清理之后,那么您的套件将很脆弱;一个测试中的错误可能会导致后续测试中误报(或误报)的传播。

有时您可以通过对环境进行分区来使测试独立。例如,如果您正在测试一个支持多个用户的应用程序,您可能希望以不同的用户身份运行每个测试(或每组测试)。

最好的选择是进行幂等的测试。我们使用一个名为nosedjango的工具来运行我们处理数据库、缓存等的所有测试。但是,这仅在您使用 django 时才有效。我敢肯定,无论您使用什么,都有等效的工具。

我一直遵循这样的理念,即当测试在全新安装上运行时,它们将为您提供最佳信息,并配备全新(空/默认填充)数据库。我什至看到过有关从保存的映像以编程方式生成新 VM 以确保两个测试运行之间尽可能少的交互的讨论。

考虑一个经典的错误“如果我添加一个用户并删除他,它会工作。如果我添加一个用户并删除他 1000 次,它会失败。” 我们都遇到过类似的错误。当我不止一次运行测试时,我希望得到完全相同的结果。由于运行程序的性质并不总是准确无误,因此速度执行可能存在一些细微差异,但它们越接近准确越好。

这意味着如果它失败了,并且我得到了失败的对象的调试输出,并且它id = 14在一次运行中出现,最好id = 14在另一次运行中出现。除非程序更改,否则决定将其变为 14 一次的任何逻辑都应使其再次变为 14。如果第二次结果是 28,我怎么知道第一次运行没有影响第二次运行?

我们生活在一个边缘,我们的产品每次都必须完美运行。通常我们有法律/合同义务这样做。这意味着我们的测试必须为我们提供尽可能多的信息。

这并不是说再次运行测试可能没有好处。本质上,是的,它应该运行完全相同(显然它可能有不同的通用 ID 和其他小的差异),如果你有空闲的 CPU 周期,可能值得再做一次。但我也有点觉得,任何会被第二次测试覆盖的东西通常都应该是一个测试用例。

tl; 博士我更喜欢不仅重置数据库,而且如果可能的话,在新机器上安装新应用程序。脚本可以轻而易举地自动执行此操作。