软件测试金字塔

软件测试金字塔
“测试金字塔”是一个隐喻,它告诉咱们将软件测试分红不一样颗粒度的桶,也给出了咱们应该在这些组中进行多少次测试的想法。尽管测试金字塔的概念已经存在了一段时间,但团队仍然很难正确地实施。本文从新探讨了测试金字塔的原始概念,并展现了如何将其付诸实践。讨论你应该在金字塔的不一样层次上寻找哪一种类型的测试,并给出了如何实现这些测试的实例。javascript

Ham Vocke前端

生产就绪软件在投入生产以前须要进行测试。vue

随着软件开发规律的成熟,软件测试方法也日趋成熟。开发团队再也不须要大量的手动软件测试人员,而是将测试工做最大部分自动化。自动化测试可让团队在短期内知道他们的软件有什么问题,而不是几天或几周。java

由自动化测试助力的缩短反馈环路与敏捷开发实践,持续交付和DevOps文化携手并进。 采用有效的软件测试方法,团队能够快速而自信地行动。react

本文探讨了全面的测试组合应该是什么样的响应,可靠和可维护的-不管你是在构建微服务架构,移动应用仍是物联网生态系统。咱们还将详细介绍构建有效和可读的自动化测试。git

(测试)自动化的重要性

软件已成为咱们生活的世界的重要组成部分。它早已超出了提升企业效率这一个目的。今天,公司都试图千方百计成为一流的数字公司。随着咱们每一个人都会与愈来愈多的软件进行交互。创新的车轮会转得更快。github

若是你想跟上步伐,必须研究如何在不牺牲质量的状况下更快地交付你的软件。持续交付是一种自动确保你的软件能够随时发布到生产环境中的方式,能够为你提供帮助。经过持续交付,能够使用构建管道自动测试软件并将其部署到测试和生产环境中。web

手动构建,测试和部署不断增长的软件数量很快就变得不可能了-除非你但愿将全部时间花费在手动,重复性的工做而不是提升交付效率的工做上。算法

软件测试金字塔

Figure 1: Use build pipelines to automatically and reliably get your software into productionspring

传统上,软件测试过于手动化,经过将应用程序部署到测试环境,而后执行一些黑盒测试,例如,经过点击你的用户界面来查看是否有任何问题。这些测试一般由测试脚本指定,以确保测试人员可以进行一致性检查。

很明显,手动测试全部更改很是耗时,重复且乏味。重复是无聊的,无聊会致使错误,并使你在本周末以前寻找不一样的工做。(译者注:意思就是没作完就找不一样的事作)
幸运的是,对于重复性任务有一种补救措施:自动化。

自动化重复性测试能够成为软件开发人员生活中的重大改变。使自动化测试,你再也不须要盲目地遵循点击协议来检查你的软件是否仍能正常工做。自动化你的测试,你能够改变代码库而不用打眼球。若是你曾经尝试过在没有适当的测试套件的状况下进行大规模的重构,我敢打赌这会是一个多么可怕的体验。 你怎么知道你是否意外地破坏了某些东西?那么,就是你点击全部的手动测试用例。但说实话:你真的喜欢这样吗?

如何作大规模的变化的时候,并知道你是否在几秒钟内破坏了东西,同时喝一口咖啡?若是你问我,这样会更愉快。

测试金字塔

若是你想认真对待软件的自动化测试,应该了解一个关键概念:测试金字塔。 迈克·科恩在他的着做“与敏捷成功”一书中提出了这个概念。这是一个伟大的视觉隐喻,告诉你思考不一样层次的测试。它还会告诉你在每一个图层上要作多少测试。

软件测试金字塔

Figure 2: The Test Pyramid

Mike Cohn的原始测试金字塔由你的测试套件应包含的三个层组成(从下到上):

一、Unit Tests

二、Service Tests

三、User Interface Tests

不幸的是,若是仔细观察,测试金字塔的概念会有点短。有人认为,麦克科恩的测试金字塔的命名或某些概念方面并不理想,我必须赞成。从现代的角度来看,测试金字塔彷佛过于简单化,所以可能会产生误导。

尽管如此,因为它的简单性,当创建本身的测试套件时,测试金字塔的本质是一个很好的经验法则。你最好的选择是记住Cohn最初的测试金字塔中的两件事:

一、用不一样的粒度编写测试

二、更高的层次,更少的测试

坚持金字塔形状,以提出一个健康,快速和可维护的测试套件:写许多小而快的单元测试。 写一些更粗粒度的测试和减小高级测试,从头至尾测试你的应用程序。 注意,你最终不会获得一个测试冰淇淋锥,这将是一个噩梦来维持,而且运行时间太长。

(译者注:测试冰激凌锥的示意图)

不要太拘泥于科恩测试金字塔中单个图层的名称。

事实上,它们可能会引发误解:服务测试是一个难以理解的术语(科恩本人谈论的观察结果是许多开发人员彻底忽略了这一层)。在诸如react,angular,ember.js等单页面应用程序框架的日子里,UI测试显然没必要位于金字塔的最高层 - 在这些框架中你彻底能够使用单元测试测试你的UI。

考虑到原始名称的缺点,只要在代码库和团队的讨论中保持一致,就能够为测试图层提供其余名称。

咱们会使用的工具和库

JUnit: test runner

Mockito:mocking dependencies

Wiremock:用于剔除外部服务

Pact:用于编写CDC测试

Selenium:用于编写UI驱动的端到端测试

REST-assured:用于编写REST API驱动的端到端测试

应用例子


我已经写了一个简单的微服务,包括一个测试套件,其中包含测试金字塔中不一样层次的测试。

示例应用程序显示了典型的微服务的特征。

它提供了一个REST接口,与数据库交互并从第三方REST服务获取信息。

它在Spring Boot中实现,即便你之前从未使用过Spring Boot,也应该能够理解。

请务必查看Github上的代码。

自述文件包含您在计算机上运行应用程序及其自动化测试所需的说明。

功能


该应用程序的功能很简单。 它提供了一个具备三个端点的REST接口:

GET / hello 返回“Hello Word”. 老是

GET / hello{lastname} 用提供的姓氏查找该人。

若是有这我的,则返回”Hello{Firstname}{Lastname}”.

GET /weather 返回当前的天气情况 Hambur, Germany.

高层级结构


在高层次上,系统具备如下结构:

软件测试金字塔

Figure 3: the high level structure of our microservice system

咱们的微服务提供了一个能够经过HTTP调用的REST接口。对于某些端点,服务将从数据库获取信息。在其余状况下,该服务将经过HTTP调用外部天气API来获取并显示当前天气情况。

内部结构


在内部,Spring服务有一个典型的Spring体系结构:

软件测试金字塔

Figure 4: the internal structure of our microservice

  • 控制器类提供REST端点并处理HTTP请求和响应

  • 存储库类与数据库接口并负责向持久存储器写入数据和从持久存储器读取数据

  • 客户端类与其余API交互,在咱们的例子中,它经过darksky.net weather API的HTTPS获取JSON

  • Domain 类捕捉咱们的domain 模型,包括领域逻辑(公平地说,在咱们的例子中,这是至关微不足道的)。

有经验的Spring开发人员可能注意到这里常用的图层缺失:受Domain-Driven Design的启发,不少开发人员构建了一个由服务类组成的服务层。我决定不在此应用程序中包含服务层。

其中一个缘由是咱们的应用程序很简单,服务层原本就是没必要要的间接层。 另一个是我认为人们过分使用服务层。我常常遇到在服务类中捕获整个业务逻辑的代码库。 Domain 模型仅仅成为数据层,而不是行为(Anemic Domain Model)。

对于每个不平凡的应用程序来讲,这会浪费不少潜能来保持代码的结构良好和可测试性,而且不能充分利用面向对象的功能。

咱们的存储库很是简单,并提供简单的CRUD功能。

为了简化代码,我使用了Spring Data。Spring Data为咱们提供了一个简单而通用的CRUD存储库实现,咱们能够使用它来代替咱们本身的实现。它还负责为测试启动内存数据库,而不是像生产中那样使用真正的PostgreSQL数据库。看看代码库,让本身熟悉内部结构。这对咱们的下一步将是有用的:测试应用程序!

单元测试


测试套件的基础将由单元测试组成。你的单元测试确保你的代码库的某个单元(你的受测主题)按预期工做。单元测试具备测试套件中全部测试的最小范围。测试套件中的单元测试数量将远远超过任何其余类型的测试。

软件测试金字塔

Figure 5: A unit test typically replaces external collaborators with test doubles

什么是单位?


若是你问三个不一样的人在单元测试中的“单位”是什么意思,你可能会收到四个不一样的,微妙的答案。在必定程度上,这是一个你本身定义的问题,没有标准答案。

若是你使用的是功能语言,一个单位极可能是一个单一的功能。你的单元测试将调用具备不一样参数的函数,并确保它返回指望值。在面向对象的语言中,单元能够从单一方法到整个类。

善于交际和孤独


有些人认为,被测主题的全部合做者(例如被测试的课程调用的其余类)都应该用模拟或存根代替,以得到完美的隔离,避免反作用和复杂的测试设置。 其余人则认为只有缓慢或反作用较大的合做者(例如,访问数据库或进行网络调用的类)应该被存根或模拟。

偶尔,人们会将这两种测试标记为孤独的单元测试,测试将全部合做者和社交单元测试存储在容许与真正合做者交谈的测试中(Jay Fields的“有效地使用单元测试工做”创造了这些术语)。若是你有空闲时间,你能够打开看一下,阅读更多关于不一样思想流派的优势和缺点。

在一天结束时,决定是否进行单独的或社交单元测试并不重要。重要的是编写自动化测试。就我我的而言,我发现本身一直都在使用这两种方法。若是使用真正的方法,合做者变得尴尬,我会慷慨地使用模拟和存根。

若是我以为参与的合做者让我对测试更有信心,那么我只会将个人服务的最外面的部分存根。

Mocking and Stubbing

Mocks和Stubs 是两种不一样类型的Test Doubles(不止这两种)。许多人能够互换地使用术语Mock和Stub。我认为在脑海中精确保持其特定属性是件好事。 你能够使用test doubles 来替换你在生产中使用的对象,并使用来帮助你进行测试的实现。简而言之,它意味着用一个假的版本替换了一件真实的东西(例如一个类,模块或函数)。假的版本看起来和行为像真实的东西(回答相同的方法调用),但你在单元测试开始时本身定义的预设回应。使用test doubles并不特定于单元测试。更精细的test doubles可用于以受控方式模拟系统的整个部分。然而,在单元测试中,你极可能会遇到不少mock和stubs(取决于你是合做或独立的开发人员),只是由于不少现代语言和库让设置变得简单和温馨。

不管你选择何种技术,极可能语言标准库或一些流行的第三方库将提供优化的安装模拟方法。 甚至从头开始编写你本身的模拟只是写一个假的类/模块/功能与真实的相同的签名,并在测试中设置假的类。

单元测试运行速度很是快。在一台情况良好的机器上,你能够在几分钟内完成数千个单元测试。单独测试小部分代码库,避免连接数据库,文件系统或触发HTTP查询(经过使用这些部分的mock和stub)来保持测试的快速。

一旦掌握了编写单元测试的窍门,你将会愈来愈流利地编写。

剔除外部协做者,设置一些输入数据,调用测试主题并检查返回的值是否与预期相符。 看看测试驱动开发,让单元测试指导你的开发; 若是正确应用,它能够帮助你进入一个良好的流程,并提出良好的可维护设计,同时自动生成全面的全自动测试套件。尽管如此,这不是银弹。还要继续,尝试一下,看看它是否适合你。

我真的须要测试这种私有方法吗?

若是你发现本身真的须要测试私有方法,那么你应该退后一步,问本身为何。
我很肯定这是一个设计问题,而不是一个范围问题。极可能你以为须要测试一个私有方法,由于它很复杂,而且经过该类的公共接口来测试这个方法须要不少尴尬的设置。
每当我发现本身处于这种情况时,我一般会得出结论,我正在测试的这个类已经太复杂了。 它作得太多,违反了单一责任原则—SOLID原则中的S。
对我而言,解决方案一般是将原始类分红两个类。 一般只须要一两分钟的思考,就能够找到一种把一个大班级分红两个小班并有我的责任的好办法。 我将私有方法(我迫切想要测试)移动到新类中,并让旧类调用新方法。 Voilà,我难以测试的私有方法如今是公开的,能够很容易地测试。最重要的是,我坚持单一责任原则改进了个人代码结构。

测试什么?


单元测试的好处在于,你能够为全部生产代码类编写单元测试,而无论它们的功能或内部结构属于哪一个层。你能够像测试存储库,域类或文件读取器同样单元测试控制器。 只需坚持one test class per production class,你就有了一个良好的开端。

单元测试类应该测试该类的公共接口

私有方法没法进行测试,由于你没法从不一样的测试类中调用它们。 受保护的或私有的包能够从测试类访问(考虑到测试类的包结构与生产类相同),但测试这些方法可能已经太过了。

编写单元测试时有一条细线:它们应该确保测试全部不重要的代码路径(包括开心路径和边缘状况)。同时它们不该该与你的实现过于紧密相关。
为何会这样?

太接近生产代码的测试很快变得使人讨厌。

只要重构生产代码(快速回顾:重构意味着更改代码的内部结构而不更改外部可见行为),你的单元测试将会中断。

这样你就失去了单元测试的一大好处:充当代码变动的安全网。你宁愿厌倦那些每次重构都会失败的愚蠢测试,这会致使更多的工做而不是帮助;并且其余人会想谁写这个愚蠢的测试?

你该作什么呢?不要在你的单元测试中反映你的内部代码结构,反而测试观察行为。将

若是我如数值 x 和 y, 结果会是 z 吗?

代替为

若是我输入x和y,该方法会先调用类A,而后调用类B,而后返回类A的结果加上类B的结果?

私有方法一般应被视为实施细节。

这就是为何你甚至不该该有试探他们的冲动。

我常常听到单元测试(或TDD)的反对者认为编写单元测试是毫无心义的工做,由于你必须测试全部的方法才能提升测试覆盖率。

他们常常引用一个情景:过于热心的团队领导迫使他们为getter和setter以及全部其余种类繁琐的代码编写单元测试,以便提供100%的测试覆盖率。
这有太多的错误。

是的,你应该测试公共接口。但更重要的是,你不要测试不重要的代码。 别担忧,Kent Beck说不要紧。你不会从测试简单的getter或setter或其余不重要的实现(例如没有任何条件逻辑)中得到任何东西。

节省时间,这是你能够参加的又一次会议,万岁!

测试结构


全部测试的良好结构(这不只限于单元测试)是这样的:

一、设置测试数据

二、在测试中调用你的方法

三、断言预期的结果被返回

记住这种结构有一个很好的助记符:“排列,行动,断言”(Arrange, Act, Assert)。 另外一个你能够使用的灵感来自BDD。

它是“给定”(given),“当”(when),“而后”(then)三合一,给出反映了设置,当方法调用,而后断言部分。

这种模式也能够应用于其余更高级别的测试。

在任何状况下,他们都能确保你的测试保持简单和一致的阅读。除此以外,考虑到这种结构的测试每每更短,更具表现力。

专业的测试助手
不管在应用程序体系结构的哪一层,你均可觉得整个代码库编写单元测试,这是一件美妙的事情。该示例显示了对控制器的简单单元测试。不幸的是,当谈到Spring的控制器时,这种方法有一个缺点:Spring MVC的控制器大量使用注释来声明他们正在监听哪些路径,使用哪些HTTP动词,他们从URL路径解析哪些参数或者查询参数等等。在单元测试中简单地调用一个控制器的方法将不会测试全部这些关键的事情。幸运的是,Spring的贡献者提出了一个很好的测试助手,能够用它来编写更好的控制器测试。确保检查出MockMVC。它给你一个很好的DSL,你能够使用它来对你的控制器发出假的请求,并检查一切都没问题。我在示例代码库中包含了一个示例。不少框架都提供了测试助手来使测试代码库的某些方面更加愉快。查看你选择的框架的文档,看看它是否为你的自动化测试提供了有用的帮助。

实施单元测试


如今咱们知道要测试什么以及如何构建单元测试,终于能够看到一个真实的例子。

咱们来看一个ExampleController类的简化版本:

@RestController
    public class ExampleController {

    private final PersonRepository personRepo;
    @Autowired
    public ExampleController(final PersonRepository personRepo) {
        this.personRepo = personRepo;
    }

    @GetMapping("/hello/{lastName}")
    public String hello(@PathVariable final String lastName) {
        Optional<Person> foundPerson = personRepo.findByLastName(lastName);
        return foundPerson
                .map(person -> String.format("Hello %s %s!",
                    person.getFirstName(),
                    person.getLastName()))
                .orElse(String.format("Who is this '%s' youre talking about?",
                    lastName));
    }
}

hello(lastname)方法的单元测试以下所示:

public class ExampleControllerTest {

    private ExampleController subject;

    @Mock
    private PersonRepository personRepo;

    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }

    @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        given(personRepo.findByLastName("Pan"))
            .willReturn(Optional.of(peter));

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Hello Peter Pan!"));
    }

    @Test
    public void shouldTellIfPersonIsUnknown() throws Exception {
        given(personRepo.findByLastName(anyString()))
            .willReturn(Optional.empty());

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Who is this 'Pan' you're talking about?"));
    }
}

咱们正在使用JUnit编写单元测试,这是Java事实上的标准测试框架。 咱们使用Mockito来替换真正的PersonRepository类和stub以供咱们测试。 这个stub容许咱们定义在这个测试中存根方法应该返回的罐头响应。

Stub使咱们的测试更加简单,可预测,而且使咱们可以轻松设置测试数据。

在安排(arrange),行动(act),断言(assert)结构以后,咱们编写了两个单元测试 - 一个正面的案例和一个被搜查的人没法找到的案例。

第一个正面的测试用例建立一个新的人物对象,并告诉模拟存储库在用“Pan”做为lastName参数的值调用时返回该对象。

测试而后继续调用应该测试的方法。 最后它断言返回值等于预期的返回值。
第二个测试的工做原理相似,但在场景中,测试方法未找到给定参数的人。

集成测试


全部非平凡的应用程序都将与其余部分(数据库,文件系统,对其余应用程序的网络调用)集成在一块儿。

在编写单元测试时,这些一般是你为了提供更好的隔离和更快的测试而遗漏的部分。 尽管如此,应用程序仍会与其余部分进行交互,并须要进行测试。集成测试能够帮助你。他们会测试应用程序与应用程序以外的全部部分的集成。

对于自动化测试,这意味着不只须要运行应用程序,还须要运行正在与之集成的组件。 若是你正在测试与数据库的集成,则须要在运行测试时运行数据库。 为了测试你能够从磁盘读取文件,须要将文件保存到磁盘并将其加载到集成测试中。

我以前提到“单元测试”是一个模糊的术语,对于“集成测试”来讲更是如此。对于某些人来讲,集成测试意味着要测试整个应用程序堆栈与系统中的其余应用程序链接。我喜欢更狭窄地对待集成测试,而且一次测试一个集成点,经过将test doubles替换为单独的服务和数据库。 结合合同测试和对test doubles运行合同测试以及真实实施,你能够提出更快,更独立而且一般更容易推理的集成测试。

狭窄的集成测试活在你服务的边界。

从概念上讲,它们始终是触发一种致使与外部部分(文件系统,数据库,单独服务)集成的操做。 数据库集成测试看起来像这样:

软件测试金字塔

Figure 6: A database integration test integrates your code with a real database

一、启动一个数据库

二、将你的应用程序链接到数据库

三、在代码中触发一个将数据写入数据库的函数

四、经过读取数据库中的数据来检查预期数据是否写入了数据库

另外一个例子,测试你的服务经过REST API与单独的服务集成多是这样的:

软件测试金字塔

Figure 7: This kind of integration test checks that your application can communicate with a separate service correctly

一、开始你的申请

二、启动单独服务的一个实例(或者具备相同接口的test double)

三、在你的代码中触发一个从独立服务的API中读取的函数

四、检查你的应用程序是否能够正确解析响应

你的集成测试 - 好比单元测试 - 能够是至关于白盒。

有些框架容许你启动应用程序,同时仍然能够模拟应用程序的其余部分,以便检查是否发生了正确的交互。编写集成测试,用于序列化或反序列化数据的全部代码段。这种状况发生的频率比你想象的要多。 想想:

  • 调用你的服务的REST API

  • 读取和写入数据库

  • 调用其余应用程序的API

  • 读取和写入队列

  • 写入文件系统

围绕这些边界编写集成测试可确保将数据写入这些外部协做者并从中读取数据能够正常工做。

在编写狭窄集成测试时,应该着眼于在本地运行外部依赖关系:启动本地MySQL数据库,对本地ext4文件系统进行测试。若是你要与单独的服务集成,请在本地运行该服务的实例,或者构建并运行模仿真实服务行为的假版本。若是没法在本地运行第三方服务,则应选择运行专用测试实例,并在运行集成测试时指向此测试实例。 避免在自动化测试中与实际生产系统集成。

将数以千计的测试请求发布到生产系统是一种绝对让人们生气的方式,由于你的日志混乱(最好的状况下),甚至DoS的服务(最坏的状况)。经过网络集成服务是普遍集成测试的典型特征,而且使测试变得更慢,一般更难以编写。

关于测试金字塔,集成测试的级别高于单元测试。

集成文件系统和数据库等慢速部件每每比运行单元测试要慢得多,而这些部件都被剔除了。毕竟,做为测试的一部分,你必须考虑外部零件的旋转,它们也可能比小而孤立的单元测试更难编写。

不过,它们的优点在于让您确信您的应用程序能够正确处理所需的全部外部部件。 单元测试没法帮助你。

数据库集成


PersonRepository是代码库中惟一的存储库类。 它依赖于Spring Data,并无实际的实现。

它只是扩展了CrudRepository接口并提供了一个单一的方法头。 其他的是Spring魔术。

public interface PersonRepository extends CrudRepository<Person, String> {
    Optional<Person> findByLastName(String lastName);
}

经过CrudRepository接口,Spring Boot经过findOne,findAll,save,update和delete方法提供了一个功能完备的CRUD存储库。

咱们的自定义方法定义(findByLastName())扩展了这个基本功能,并为咱们提供了一种按姓氏提取PersonS的方法。 Spring Data分析了方法的返回类型及其方法名称,并根据命名约定检查方法名称以找出它应该作什么。

虽然Spring Data负责实现数据库存储库,但我仍然编写了一个数据库集成测试。 你可能会争辩说,这是测试框架和我应该避免的,由于它不是咱们正在测试的代码。 不过,我相信至少有一个集成测试是相当重要的。首先它测试咱们的自定义findByLastName方法的行为如预期。

其次,它证实咱们的存储库正确使用了Spring的接线并能够链接到数据库。
为了让你在机器上运行测试变得容易(无需安装PostgreSQL数据库),咱们的测试链接到内存中的H2数据库。

我已经在build.gradle文件中将H2定义为测试依赖项。test目录中的application.properties没有定义任何spring.datasource属性。 这告诉Spring Data使用内存数据库。由于它在类路径上发现H2,因此它在运行咱们的测试时仅使用H2。

当使用int配置文件运行实际应用程序时(例如,经过将SPRING_PROFILES_ACTIVE = int设置为环境变量),它将链接到application-int.properties中定义的PostgreSQL数据库。

我知道,要了解和理解这些Spring细节是很是多的。为了达到目的,你必须筛选大量的文档。由此产生的代码很容易理解,但若是你不了解Spring的细节,就很难理解。

除此以外,使用内存数据库是危险的业务。

毕竟,咱们的集成测试针对的是不一样于生产环境的不一样类型的数据库。 继续并自行决定是否更喜欢使用Spring魔术方法和简单的代码,而不是更明确而更详细的实现。

已经有足够的解释了,下面是一个简单的集成测试,它将一个Person保存到数据库中,并经过姓氏找到它:

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {
    @Autowired
    private PersonRepository subject;

    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }

    @Test
    public void shouldSaveAndFetchPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        subject.save(peter);

        Optional<Person> maybePeter = subject.findByLastName("Pan");

        assertThat(maybePeter, is(Optional.of(peter)));
    }
}

你能够看到,咱们的集成测试遵循与单元测试相同的arrange(排列),act(行为)和assert(断言)结构。告诉你,这是一个广泛的概念!

与独立服务集成


咱们的微服务与darksky.net,一个天气REST API交互。固然,咱们但愿确保咱们的服务可以正确地发送请求并解析响应。
咱们但愿在运行自动化测试时避免碰到真正的darksky服务器。

咱们免费计划的配额限制只是缘由的一部分。 真正的缘由是解耦。 咱们在darksky.net的测试应该独立于其余人。

即便你的机器没法访问darksky服务器或darksky服务器因维护而停机。
在运行咱们的集成测试时,能够经过运行咱们本身的虚假darksky服务器来避免碰到真正的darksky服务器。 这听起来像是一项艰巨的任务。

因为像Wiremock这样的工具,这很容易。 看这个:

@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientIntegrationTest {

    @Autowired
    private WeatherClient subject;

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8089);

    @Test
    public void shouldCallWeatherService() throws Exception {
        wireMockRule.stubFor(get(urlPathEqualTo("/some-test-api-key/53.5511,9.9937"))
                .willReturn(aResponse()
                        .withBody(FileLoader.read("classpath:weatherApiResponse.json"))
                        .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withStatus(200)));

        Optional<WeatherResponse> weatherResponse = subject.fetchWeather();

        Optional<WeatherResponse> expectedResponse = Optional.of(new WeatherResponse("Rain"));
        assertThat(weatherResponse, is(expectedResponse));
    }
}

要使用Wiremock,咱们在固定端口(8089)上实例化一个WireMockRule。 使用DSL能够设置Wiremock服务器,定义它应该监听的端点,并设置它应该响应的灌装响应(canned responses)。

接下来咱们调用想要测试的方法,即调用第三方服务的方法,并检查结果是否正确解析。

了解测试如何知道应该调用虚拟的Wiremock服务器而不是真正的darksky API很是重要。 秘密在咱们包含在src / test / resources中的application.properties文件中。这是运行测试时Spring加载的属性文件。在这个文件中,咱们覆盖了像API键和URLs这样的配置,其值适合咱们的测试目的,例如调用虚拟的Wiremock服务器而不是真正的服务器:

weather.url = http://localhost:8089

请注意,这里定义的端口必须与咱们在测试中实例化WireMockRule时所定义的端口相同。 经过在咱们的WeatherClient类的构造函数中注入URL,能够将测试中的真实天气API的URL替换为假天气:

@Autowired
public WeatherClient(final RestTemplate restTemplate,
                     @Value("${weather.url}") final String weatherServiceUrl,
                     @Value("${weather.api_key}") final String weatherServiceApiKey) {
    this.restTemplate = restTemplate;
    this.weatherServiceUrl = weatherServiceUrl;
    this.weatherServiceApiKey = weatherServiceApiKey;
}

这样,咱们的WeatherClient从应用程序属性中定义的weather.url属性中读取weatherUrl参数的值。

使用Wiremock等工具为单独服务编写narrow integration tests很是简单。 不幸的是,这种方法有一个缺点:咱们如何确保咱们设置的假服务器的行为像真正的服务器?

在目前的实施中,单独的服务可能会改变其API,咱们的测试仍然会经过。 如今咱们只是测试咱们的WeatherClient能够解析假服务器发送的响应。 这是一个开始,但很是脆弱。

使用端到端测试并针对真实服务的测试实例运行测试而不是使用假服务能够解决此问题,但会使咱们依赖于测试服务的可用性。

幸运的是,有一个更好的解决方案来解决这个困境:对虚假服务器和真实服务器运行合同测试可确保咱们在集成测试中使用的虚假测试是忠实的测试。 让咱们看看接下来的工做。

合同测试


更多的现代软件开发组织已经找到了经过跨不一样团队开发系统来扩展其开发工做的方法。 个别团队创建个别的,松散耦合的服务,而不用彼此踩脚趾,并将这些服务整合到一个大的,有凝聚力的系统中。

最近围绕微服务的讨论正是关注这一点。

将系统分割成许多小型服务经常意味着这些服务须要经过某些(但愿定义明确的,有时意外增加的)接口相互通讯。

不一样应用程序之间的接口能够有不一样的形状和技术。 常见的是

  • REST和JSON经过HTTPS

  • 使用相似gRPC的RPC

  • 使用队列构建事件驱动的体系结构

对于每一个接口,涉及两方:提供者和消费者。 该提供商向消费者提供数据。 消费者处理从提供者处得到的数据。

在REST世界中,提供者使用全部必需的端点构建REST API;

消费者调用此REST API来获取数据或触发其余服务中的更改。

在异步的,事件驱动的世界中,提供者(一般称为发布者)将数据发布到队列中; 消费者(一般称为订户)订阅这些队列并读取和处理数据。

软件测试金字塔

Figure 8: Each interface has a providing(or publishing) and a consuming(or subscribing) party. The specification of an interface can be considered a contract.

因为你常常在不一样团队之间传播消费和提供服务,你会发现本身处于必须明确指定这些服务之间的接口(所谓的合同)的状况。 传统上,公司经过如下方式来解决这个问题:

  • 编写一份详细的长期界面规范(合同)

  • 按照定义的合同实施提供服务

  • 将界面规范扔到围栏上的消费团队

  • 等到他们实现他们消费接口的部分

  • 运行一些大规模的手动系统测试,看看是否一切正常

  • 但愿两个团队永远坚持界面定义,不要搞砸了

更现代化的软件开发团队用更自动化的东西取代了第5步和第6步:自动契约测试确保消费者和提供者方面的实现仍然坚持已定义的合同。他们做为一个很好的回归测试套件,并确保早期发现与合同的误差。

在一个更敏捷的组织中,你应该采起更有效和浪费更少的路线。你在同一个组织内构建您的应用程序。直接与其余服务的开发人员直接交谈,而不是摒弃过于详细的文档,这不该该太难。毕竟他们是你的同事,而不是第三方供应商,你只能经过客户支持或法律上的防弹合同进行交谈。

消费者驱动合同测试(CDC测试)让消费者推进合同的实施。使用CDC,接口的使用者编写测试,从接口检查接口所需的全部数据。而后消费团队发布这些测试,以便发布团队能够轻松获取并执行这些测试。支援团队如今能够经过运行CDC测试来开发他们的API。一旦全部测试经过,他们知道已经实施了消费团队所需的一切。

软件测试金字塔

Figure 9: 合同测试确保接口的提供者和全部消费者都坚持已定义的接口契约。 经过CDC测试,接口的消费者以自动化测试的形式发布他们的需求;提供者不断地获取并执行这些测试
这种方法容许提供团队只实施真正必要的事情(保持简单,YAGNI(You ain’t gonna need it)等等)。

提供界面的团队应持续(在他们的构建流水线中)获取并运行这些CDC测试,以当即发现任何重大更改。

若是他们更改界面,他们的CDC测试将会失败,从而阻止突发变化的发生。 只要测试保持绿色,团队能够进行他们喜欢的任何更改,而没必要担忧其余团队。 消费者驱动的合同方法会给你带来一个看起来像这样的过程:

  • 消费团队编写符合全部消费者指望的自动化测试

  • 他们为提供团队发布测试

  • 提供团队持续运行CDC测试并保持绿色

  • 一旦CDC测试中断,两个团队都会互相交流

若是你的组织采用微服务方法,进行CDC测试是创建自治团队的重要一步。 CDC测试是促进团队沟通的自动化方式。

他们确保团队之间的界面随时都在工做。

CDC测试失败是一个很好的指标,你应该走到受影响的团队,聊聊任何即将到来的API变化,并了解你想如何前进。

一个原始的CDC测试实现能够像对API发起请求同样简单,并声明响应包含你须要的全部东西。而后将这些测试打包为可执行文件(.gem,.jar,.sh),并将其上传到其余团队能够获取的地方(例如Artifactory等工件存储库)。

在过去的几年中,CDC方法变得愈来愈流行,而且已经构建了几种工具来使它们更容易编写和交换。

Pact多是最近最突出的一个。

它具备为消费者和提供商编写测试的复杂方法,可为你提供开箱即用的独立服务存根,并容许您与其余团队交换CDC测试。

Pact已经被移植到不少平台上,而且能够与JVM语言,Ruby,.NET,JavaScript等一块儿使用。

若是您想开始使用CDC而且不知道如何,Pact能够是一个理智的选择。

这些文档可能会在第一时间压倒一切。

保持耐心,并努力经过它。它有助于深刻了解疾病预防控制中心,从而使您在与其余团队合做时更容易倡导使用疾病预防控制中心。

消费者驱动的合同测试(CDC)能够成为一个真正的游戏规则改变者,以创建自信的团队,能够快速而自信地行动。

帮你本身一个忙,阅读这个概念并试一试。

一套可靠的CDC测试对于可以快速移动而不会破坏其余服务并对其余团队形成很大的挫折,这个测试是无价的。

消费者测试(咱们的团队)


咱们的微服务使用天气API。

所以,咱们有责任编写一份消费者测试,以肯定咱们对微服务与天气服务之间的合同(API)的指望。

首先,咱们在build.gradle中包含一个用于编写契约消费者测试的库:
testCompile('au.com.dius:pact-jvm-consumer-junit_2.11:3.5.5')
感谢这个库,咱们能够实现一个消费者测试并使用pact的模拟服务:

@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientConsumerTest {

    @Autowired
    private WeatherClient weatherClient;

    @Rule
    public PactProviderRuleMk2 weatherProvider =
            new PactProviderRuleMk2("weather_provider", "localhost", 8089, this);

    @Pact(consumer="test_consumer")
    public RequestResponsePact createPact(PactDslWithProvider builder) throws IOException {
        return builder
                .given("weather forecast data")
                .uponReceiving("a request for a weather request for Hamburg")
                    .path("/some-test-api-key/53.5511,9.9937")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(FileLoader.read("classpath:weatherApiResponse.json"),
                            ContentType.APPLICATION_JSON)
                .toPact();
    }

    @Test
    @PactVerification("weather_provider")
    public void shouldFetchWeatherInformation() throws Exception {
        Optional<WeatherResponse> weatherResponse = weatherClient.fetchWeather();
        assertThat(weatherResponse.isPresent(), is(true));
        assertThat(weatherResponse.get().getSummary(), is("Rain"));
    }
}

若是仔细观察,你会看到WeatherClientConsumerTest与WeatherClientIntegrationTest很是类似。此次咱们不使用Wiremock做为服务器stub,而是使用Pact。事实上,消费者测试与集成测试彻底同样,咱们用一个stub替换真正的第三方服务器,定义指望的响应并检查咱们的客户端是否能够正确解析响应。在这个意义上,WeatherClientConsumerTest自己就是一个小范围的集成测试。与基于线链接的测试相比,这种测试的优势是每次运行时都会生成一个pact文件(在target / pacts /&pact-name>.json中找到)。该协议文件以特殊的JSON格式描述了咱们对合同的指望。而后能够使用此协议文件来验证咱们的存根服务器的行为与真实服务器的行为相同。咱们能够将协议文件交给提供界面的团队。他们拿这个协议文件,并使用在那里定义的指望写一个提供者测试。这样他们测试他们的API是否知足咱们全部的指望。

你会发现这是CDC消费者驱动部分的来源。

消费者经过描述他们的指望来推进接口的实现。提供者必须确保他们可以知足全部的指望,而且他们完成了。 没有镀金,没有YAGNI和东西。
将协议文件提供给提供团队能够经过多种方式进行。一个简单的方法是将它们放入版本控制并告诉提供者团队老是获取最新版本的协议文件。更多的进步是使用工件存储库,像亚马逊S3或协议代理的服务。

开始简单并根据须要增加。

在你的真实世界的应用程序中,你不须要二者,一个集成测试和一个客户端类的消费者测试。示例代码库包含两个向你展现如何使用任何一个。若是你想使用pact编写CDC测试,我建议坚持使用后者。编写测试的效果是同样的。使用pact的好处是,您能够自动得到一份pact文件,其中包含对其余团队能够轻松实施其供应商测试的合同指望。固然,若是你能说服其余团队也使用pact,这是惟一有意义的。若是这不起做用,使用集成测试和Wiremock组合是一个体面的计划b。

提供者测试(另外一个团队)


提供者测试必须由提供天气API的人员执行。咱们正在使用dark sky.net提供的公共API。理论上,darksky team 将在他们的最后实施提供商测试,以检查他们是否违反了他们的应用程序和咱们的服务之间的合同。

显然,他们不关心咱们微不足道的示例应用程序,也不会为咱们实施CDC测试。这是面向公众的API和采用微服务的组织之间的巨大差别。面向公众的API不可能考虑每一个用户,不然他们将没法前进。在你本身的组织中,能够并且应该。你的应用极可能会为少数几个用户提供服务,最多可能有几十个用户。为了保持稳定的系统,会很好地编写这些接口的提供者测试。

提供团队获取pact文件并针对其提供的服务运行该文件。为此,他们实现了一个提供程序测试,读取该文件,存储一些测试数据,并根据他们的服务运行在pact文件中定义指望值。

Pact伙伴已经编写了几个库来执行提供者测试。他们的主要GitHub repo为你提供了一个很好的概览,哪一个消费者和哪些提供程序库可用。 选择最适合你的技术堆栈的那个。

为了简单起见,咱们假设darksky API也是在Spring Boot中实现的。 在这种状况下,他们能够使用Spring的pact 提供者,它很好地钩入Spring的MockMVC机制。 darksky.net团队将执行的假设提供者测试可能以下所示:

@RunWith(RestPactRunner.class)
@Provider("weather_provider") // same as the "provider_name" in our clientConsumerTest
@PactFolder("target/pacts") // tells pact where to load the pact files from
public class WeatherProviderTest {
    @InjectMocks
    private ForecastController forecastController = new ForecastController();

    @Mock
    private ForecastService forecastService;

    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();

    @Before
    public void before() {
        initMocks(this);
        target.setControllers(forecastController);
    }

    @State("weather forecast data") // same as the "given()" in our clientConsumerTest
    public void weatherForecastData() {
        when(forecastService.fetchForecastFor(any(String.class), any(String.class)))
                .thenReturn(weatherForecast("Rain"));
    }
}

你会看到全部提供程序测试必须执行的操做是加载一个pact文件(例如,经过使用@PactFolder注释来加载之前下载的协议文件),而后定义应如何提供预约义状态的测试数据(例如,使用Mockito mocks)。没有定制测试能够被实施。这些都来自pact文件。Provider test 与消费者测试中声明的provider name和状态匹配的对应对象是很是重要的。

Provider Test(our team)


咱们已经看到如何测试咱们的服务和天气提供商之间的合同。有了这个接口咱们的服务做为消费者,天气服务就像提供者同样。进一步思考会看到,咱们的服务还充当其余人的提供者:提供了一个REST API,它准备好供其余人使用的端点。

正如刚刚了解的那样,合同测试很是激烈,咱们固然也会为这份合同写一份合同测试。 幸运的是,正在使用消费者驱动契约(consumer-driven contracts),所以全部消费团队都向咱们发送他们的Pacts,咱们能够使用它们来为咱们的REST API实现提供者测试。

首先,将Spring的Pact提供程序库添加到项目中:

testCompile('au.com.dius:pact-jvm-provider-spring_2.12:3.5.5')

实现提供者测试的方式与以前描述的相同。为了简单起见,我将咱们的简单消费者的pact文件输入到咱们服务的存储库中。这使得目的更容易,在真实场景中,你可能会使用更复杂的机制来分发你的pact文件。

@RunWith(RestPactRunner.class)
@Provider("person_provider")// same as in the "provider_name" part in our pact file
@PactFolder("target/pacts") // tells pact where to load the pact files from
public class ExampleProviderTest {

    @Mock
    private PersonRepository personRepository;

    @Mock
    private WeatherClient weatherClient;

    private ExampleController exampleController;

    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();

    @Before
    public void before() {
        initMocks(this);
        exampleController = new ExampleController(personRepository, weatherClient);
        target.setControllers(exampleController);
    }

    @State("person data") // same as the "given()" part in our consumer test
    public void personData() {
        Person peterPan = new Person("Peter", "Pan");
        when(personRepository.findByLastName("Pan")).thenReturn(Optional.of
                (peterPan));
    }
}

所示的ExampleProviderTest须要根据咱们提供的pact文件提供状态,就是这样。一旦运行提供程序测试,Pact就会拿起pact文件并针对咱们的服务发起HTTP请求,而后根据设置的状态作出响应。

UI Tests


大多数应用程序都有某种用户界面。

一般,咱们正在讨论Web应用程序环境中的Web界面。人们常常忘记REST API或命令行界面与花哨的Web用户界面同样多的用户界面。

UI tests测试应用程序的用户界面是否正常工做。用户输入应该触发正确的操做,数据应该呈现给用户,UI状态应该按预期改变。

软件测试金字塔

UI测试和端到端测试有时(如Mike Cohn的案例)被认为是一回事。

对我来讲,这是两个相互正交的概念。

是的,端到端测试你的应用程序一般意味着经过用户界面来驱动您的测试。

然而,反过来倒是不正确的。

测试你的用户界面不必定要以端到端的方式进行。

根据你使用的技术,测试用户界面可能很是简单,只需为后端JavaScript代码编写一些单元测试并将其后端代码删除便可。

使用传统的Web应用程序测试用户界面能够使用像Selenium这样的工具来实现。若是你认为REST API是你的用户界面,应该经过围绕API编写适当的集成测试来得到所需的一切。

有了Web界面,可能须要在UI中测试多个方面:行为,布局,可用性,不多对公司设计的测试。

幸运的是,测试用户界面的行为很是简单。你点击这里,在那里输入数据,并但愿用户界面的状态相应地改变。现代的单页面应用程序框架(react,vue.js,Angular等)一般带有本身的工具和helpers,它们容许您以至关低级的(单元测试)方式完全测试这些交互。即便你使用vanilla javascript来实现本身的前端实现,你也能够使用常规的测试工具,如Jasmine或Mocha。使用更传统的服务器端渲染应用程序,基于Selenium的测试将是你的最佳选择。

测试你的web应用程序的布局是否保持无缺,有点困难。根据你的应用程序和你的用户需求,可能须要确保代码更改不会意外地破坏网站的布局。
问题在于,计算机在检查某些“看起来不错”(多是一些聪明的机器学习算法可能在未来改变)方面是很是糟糕的。

若是你想要在构建管道中自动检查Web应用程序的设计,有一些工具可供尝试。这些工具中的大多数都利用Selenium以不一样的浏览器和格式打开您的Web应用程序,截取屏幕截图并将它们与之前拍摄的截图进行比较。若是新旧截图以意想不到的方式出现差别,该工具会通知您。

Galen是这些工具之一。

可是,若是您有特殊要求,即便推出本身的解决方案也不难。和我有合做的一些团队已经创建了阵容和基于Java的表哥jimupup来实现相似的功能。两种工具都采用了我以前描述的基于Selenium的方法。

一旦你想测试可用性和“看起来不错”的因素,你就离开了自动化测试领域。这是你应该依赖探索性测试,可用性测试(这甚至能够像走廊测试同样简单)的领域,并向用户展现他们是否喜欢使用你的产品,而且能够使用全部功能而不会感到沮丧或烦恼。

端到端测试


经过用户界面测试已部署的应用程序是你能够测试应用程序的最为端到端的方式。 前面描述的webdriver驱动的UI测试是端到端测试的一个很好的例子。

软件测试金字塔

当你须要肯定软件是否正常工做时,端到端测试(也称为普遍堆栈测试)为你提供最大的信心。经过Selenium和WebDriver协议,你能够经过自动驱动(无头)浏览器针对部署的服务,执行点击操做,输入数据并检查用户界面的状态来自动执行测试。您能够直接使用Selenium或使用基于它的工具,Nightwatch就是其中之一。

端到端测试带来了各自的问题。

它们是出了名的碎片,每每因意外和不可预见的缘由而失败。他们的失败每每是一种误解。你的用户界面越复杂,测试越碎片。浏览器的怪癖,计时问题,动画和意外的弹出对话框只是让我花更多时间进行调试的一些缘由,而不是我想认可的。

在微服务世界中,谁负责编写这些测试也是一个大问题。因为它们跨越多个服务(整个系统),所以没有一个团队负责编写端到端测试。

若是你有一个集中的质量保证团队,他们看起来很合适。

然而,拥有一个集中的质量保证团队是一个很大的反模式,在DevOps世界里不该该有一席之地,你的团队是真正意义上的跨职能团队。谁应该拥有端到端的测试并不容易。也许你的组织有一个实践社区或一个能够照顾这些的高质量公会。找到正确的答案在很大程度上取决于你的组织。

此外,端到端测试须要大量维护,运行速度很是缓慢。

考虑到不止一两个微服务的格局,你甚至没法在本地运行端到端测试-由于这须要在本地启动全部微服务。在你的开发机器上启动了数百个应用程序,而不会炸毁你的RAM。

因为维护成本高昂,应该尽可能减小端到端测试的数量。

考虑用户在应用程序中使用的高价值交互。

尝试提出定义产品核心价值的用户旅程,并将这些用户旅程中最重要的步骤转化为自动化的端到端测试。

若是你正在创建一个电子商务网站,你最有价值的客户旅程多是一个用户搜索产品,将其放入购物篮并结账。仅此而已。

只要这个旅程仍然有效,不该该太麻烦。

也许你会发现一两个更重要的用户旅程,能够将其转化为端到端测试。

除此以外,更多的事情均可能更痛苦。

请记住:你的测试金字塔中有不少较低的级别,已经测试了各类边界案例并与系统的其余部分进行了集成。 没有必要在更高层次上重复这些测试。 高昂的维护工做量和大量的误报会减慢你的速度,会让你在测试中失去信心,宜早不宜迟。

User Interface End-to-End Test


对于端到端测试,Selenium和WebDriver协议是许多开发人员首选的工具。 使用Selenium,您能够选择一个你喜欢的浏览器,而后让它自动调用你的网站,点击界面这里和那里,输入数据并检查用户界面中的变化。
Selenium须要一个能够启动并用于运行测试的浏览器。对于不一样的浏览器,能够使用多个所谓的“驱动程序”。

选择一个(或多个)并将其添加到您的build.gradle。

不管你选择哪一种浏览器,都须要确保团队中的全部开发人员和你的CI服务器在本地安装了正确版本的浏览器。保持同步可能会很是痛苦。对于Java,有一个很好的小型库叫作webdrivermanager,它能够自动下载并设置你想要使用的正确版本的浏览器。将这两个依赖关系添加到你的build.gradle中,而后能够继续:

testCompile('org.seleniumhq.selenium:selenium-chrome-driver:2.53.1')
testCompile('io.github.bonigarcia:webdrivermanager:1.7.2')

在测试套件中运行完整的浏览器可能会很麻烦。

特别是在使用持续交付时,运行管道的服务器可能没法启动包含用户界面的浏览器(例如由于没有X-Server可用)。

您能够经过启动像xvfb这样的虚拟X-Server来解决此问题。

最近的方法是使用无头浏览器(即没有用户界面的浏览器)来运行webdriver测试。 直到最近PhantomJS是领先的自动化的无头浏览器。

自从Chromium和Firefox宣布他们在浏览器中实现无头模式后,PhantomJS忽然变得过期了。

毕竟,最好使用用户实际使用的浏览器(好比Firefox和Chrome)来测试网站,而不是仅仅做为开发人员方便你使用仿真浏览器。

无头的Firefox和Chrome都是全新的,而且还没有被普遍采用来执行webdriver测试。 咱们想保持简单。

而不是摆弄一望无际的模式,让咱们坚持使用Selenium和普通浏览器的经典方式吧。 一个简单的端到端测试,使用Chrome浏览器,导航到咱们的服务,并检查网站的内容以下所示:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloE2ESeleniumTest {

    private WebDriver driver;

    @LocalServerPort
    private int port;

    @BeforeClass
    public static void setUpClass() throws Exception {
        ChromeDriverManager.getInstance().setup();
    }

    @Before
    public void setUp() throws Exception {
        driver = new ChromeDriver();
    }

    @After
    public void tearDown() {
        driver.close();
    }

    @Test
    public void helloPageHasTextHelloWorld() {
        driver.get(String.format("http://127.0.0.1:%s/hello", port));

        assertThat(driver.findElement(By.tagName("body")).getText(), containsString("Hello World!"));
    }
}

请注意,若是你在运行此测试的系统(本地计算机,你的CI服务器)上安装了Chrome,该测试将仅在你的系统上运行。测试很简单。它使用@SpringBootTest在一个随机端口上运行整个Spring应用程序。而后,咱们实例化一个新的Chrome浏览器驱动程序,告诉它导航到咱们的微服务的/ hello端点,并检查它是否在浏览器窗口中打印出“Hello World!”。这是很酷的东西!

REST API End-to-End Test


在测试应用程序时避免使用图形用户界面是一个好主意,它能够提供比较完整的端到端测试,同时仍涵盖应用程序堆栈的大部份内容。当经过应用程序的Web界面进行测试特别困难时,这能够派上用场。 也许你甚至没有一个Web UI,而是提供一个REST API来代替(由于你有一个单独的页面应用程序在某个地方与该API交谈,或者仅仅是由于你鄙视一切都很好)。 不管哪一种方式,一个Subcutaneous Test,只是在图形用户界面下进行测试,而且可让你真正走远,而不会对信心形成太大损失。 若是你像咱们的示例代码那样提供REST API,那就是正确的作法:

@RestController
public class ExampleController {
    private final PersonRepository personRepository;

    // shortened for clarity

    @GetMapping("/hello/{lastName}")
    public String hello(@PathVariable final String lastName) {
        Optional<Person> foundPerson = personRepository.findByLastName(lastName);

        return foundPerson
             .map(person -> String.format("Hello %s %s!",
                     person.getFirstName(),
                     person.getLastName()))
             .orElse(String.format("Who is this '%s' you're talking about?",
                     lastName));
    }
}

当我测试一个提供REST API的服务时,让我再向您展现一个更方便的库。

REST-assured

是一个为你提供一个很好的DSL的库,用于发送针对API的实际HTTP请求并评估你收到的响应。

首先要作的事情是:将依赖关系添加到build.gradle中。
testCompile('io.rest-assured:rest-assured:3.0.3')
借助这个库,咱们能够为咱们的REST API实施端到端测试:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloE2ERestTest {

    @Autowired
    private PersonRepository personRepository;

    @LocalServerPort
    private int port;

    @After
    public void tearDown() throws Exception {
        personRepository.deleteAll();
    }

    @Test
    public void shouldReturnGreeting() throws Exception {
        Person peter = new Person("Peter", "Pan");
        personRepository.save(peter);

        when()
                .get(String.format("http://localhost:%s/hello/Pan", port))
        .then()
                .statusCode(is(200))
                .body(containsString("Hello Peter Pan!"));
    }
}

再次,咱们使用@SpringBootTest启动整个Spring应用程序。

在这种状况下,咱们@Autowire PersonRepository,以便咱们能够轻松地将测试数据写入咱们的数据库。 当咱们如今要求REST API向咱们的朋友“潘先生”说“打招呼”时,咱们会获得一个很好的问候。 很是好! 若是你甚至没有运行网络界面,那么就能够进行足够多的端到端测试。

Acceptance Tests — Do Your Features Work Correctly?


在测试金字塔中移动得越高,进入测试领域的可能性就越大,从用户的角度来看,你构建的功能是否正常工做。

能够将你的应用程序视为黑盒子,并将测试中的焦点从下面中移除

当我输入值x和y时,返回值应该是z

而是用

由于有一个登陆用户(given there's a logged in user)

还有一篇文章“自行车”(and there's an article "bicycle")

当用户导航到“自行车”文章的详细页面时(when the user navigates to the "bicycle" article's detail page)

并点击“添加到篮子”按钮(and clicks the "add to basket" button)

那么文章“自行车”应该在他们的购物篮中(then the article "bicycle" should be in their shopping basket)

有时你会听到这些测试的功能测试或验收测试的条款。

有时人们会告诉你功能和验收测试是不一样的东西。这些术语是混淆的。甚至有时候人们会无休止地讨论措辞和定义。一般这种讨论会引发至关大的混乱。这才重要:在某一时刻,你应该确保从用户的角度测试软件是否正常工做,而不只仅是从技术角度。 你认为这些测试真的不是那么重要。

然而,进行这些测试是有必要的。

选择一个,坚持下去,而后编写这些测试。

这也是人们谈论BDD和使您可以以BDD方式实施测试的工具的时刻。

BDD或BDD风格的编写测试方式多是一个不错的窍门,可将你的思想从实施细节转移到用户需求。 继续尝试吧。

你甚至不须要像Cucumber那样采用全面的BDD工具(尽管你能够)。

有些断言库(好比chai.js容许你用should样式的关键字来编写断言,这样可让你的测试可以读取更多相似于BDD的内容。即便你不使用提供这种表示法的库,聪明且分工合理的代码 将容许你编写以用户行为为中心的测试。一些辅助方法/函数能够为你带来很长的路要走:

# a sample acceptance test in Python

def test_add_to_basket():
    # given
    user = a_user_with_empty_basket()
    user.login()
    bicycle = article(name="bicycle", price=100)

    # when
    article_page.add_to_.basket(bicycle)

    # then
    assert user.basket.contains(bicycle)

验收测试能够有不一样的粒度级别。

大多数时候他们将会至关高级并经过用户界面测试您的服务。然而,理解在技术上不须要在测试金字塔的最高级别编写验收测试是很好的。

若是你的应用程序设计和手头的场景容许您在较低的级别上编写验收测试,那就去作吧。 进行低级测试比进行高级测试要好。 验收测试的概念 - 证实功能为用户正确地工做 - 彻底与测试金字塔正交。

Exploratory Testing


即便是最用功的自动化测试也不完美。 有时候你会错过自动化测试中的某些边缘状况。 有时经过编写单元测试来检测特定的错误几乎是不可能的。 某些质量问题在您的自动化测试中甚至不明显(考虑设计或可用性)。 尽管你对测试自动化有着最好的意图,但某些类型的手动测试仍然是一个好主意。

软件测试金字塔
Figure 12: Use exploratory testing to spot all quality issues that your build pipeline didn’t spot

在测试组合中包含探索性测试。 这是一种手动测试方法,强调测试人员的自由和创造力,以便在运行中的系统中发现质量问题。

只需按期安排一些时间,卷起袖子并尝试破坏应用程序。

使用破坏性的思惟方式,想出办法在应用程序中引起问题和错误。 记录您之后找到的全部内容。

注意错误,设计问题,响应时间缓慢,丢失或误导性的错误信息以及其余一切会让你做为软件用户烦恼的事情。

好消息是,你能够使用自动化测试你大部分发现。为你发现的错误编写自动化测试,确保未来不会出现该错误的任何回退。此外,它还能够帮助在错误修复期间缩小问题的根源。

在探索性测试过程当中,你会发现经过你的构建管道未被注意到的问题。不要感到沮丧。 这对您的构建管道的成熟度有很好的反馈。

与任何反馈同样,请务必采起行动:考虑你未来能够采起什么措施来避免这些问题。也许你错过了一些自动化测试。

也许在此次迭代中对自动化测试嗤之以鼻,而且须要在未来进行更完全的测试。 也许有一种闪亮的新工具或方法能够用来避免未来出现这些问题。

请务必采起行动,以便管道和整个软件交付将走得更远变得更加成熟。

关于Testing Terminology的结论


谈论不一样的测试分类老是很困难。

当我谈论单元测试时,个人意思可能与你的理解稍有不一样。

若是是集成测试,状况更糟。

对于某些人来讲,集成测试是一项很是普遍的活动,能够测试整个系统的许多不一样部分。 对我而言,这是一个至关狭隘的东西,一次只测试一个外部部件的集成。 一些人称他们为集成测试,一些人称他们为组件测试,一些人更喜欢术语服务测试。

甚至其余人也会争辩说,全部这三个术语都是彻底不一样的东西。 没有对错。

软件开发社区根本没有设法围绕测试定义明确的术语。

不要太拘泥于模棱两可的话。

若是您称之为端到端或普遍的堆栈测试或功能测试,则可有可无。

若是你的集成测试对你来讲意味着与其余公司的人不一样,那就不要紧了。 是的,若是咱们的专业可以按照一些明肯定义的条件解决而且所有坚持下去,那将会很是好。 不幸的是,这尚未发生。

并且,因为在编写测试时有不少细微差异,反而比一堆离散的存储桶更像一个频谱,这使得一致的命名更加困难。

重要的是,你应该找到适合你和你的团队的条款。清楚你想写的不一样类型的测试。 就团队中的命名达成一致,并就每种类型的测试范围达成共识。 若是你在团队内部(或者甚至在你的组织内)得到这种一致性,那么你应该关心的就是这些。 当Simon Stewart描述他们在Google使用的方法时,Simon Stewart总结得很是好。

并且我认为这彻底代表,让名字和命名惯例过于沉闷是不值得的麻烦。

Putting Tests Into Your Depolyment Pipeline


若是你使用的是持续集成或持续交付,那么将拥有一个部署管道,每次对软件进行更改时都会运行自动化测试。

一般这个管道分红几个阶段,逐渐让你更加确信软件已准备好部署到生产环境。 听到全部这些不一样类型的测试,你可能想知道如何将它们放置在部署管道中。 要回答这个问题,应该考虑持续交付(其实是极限编程和敏捷软件开发的核心价值之一)的基本价值之一:快速反馈。
一个好的构建管道告诉你,尽量快地搞砸。你不想等一个小时才能发现你的最新代码更改破坏了一些简单的单元测试。若是你的管道须要很长时间才能给反馈,那么你极可能已经回家了。经过快速运行的测试放在流水线的早期阶段,能够在几秒钟内得到这些信息,也可能须要几分钟。相反,在较晚的阶段,较长时间的运行测试(一般是较宽范围的测试)放在不会推迟快速运行测试的反馈。你会发现定义部署管道的阶段不是由测试类型驱动的,而是由速度和范围决定的。考虑到这一点它能够是一个很是合理的决定,把一些真正的狭义范围的和快速运行的集成测试在同一个舞台上你的单元测试 - 仅仅是由于他们给你更快的反馈,而不是由于你想画沿着你的测试的正式类型。

Avoid Test Duplication


如今你知道你应该写出不一样类型的测试,但还有一个能够避免的错误:在金字塔的不一样层次上重复测试。

虽然你的直觉可能会说不须要太多的测试。我向你保证,须要。

测试套件中的每一项测试都须要额外的时间,并非免费的。

编写和维护测试须要时间。 阅读和理解其余人的测试须要时间。

固然,运行测试须要时间。

与生产代码同样,应该尽可能简化并避免重复。

在实施你的测试金字塔的背景下,你应该记住两条经验法则:

一、若是较高级别的测试发现错误,而且没有较低级别的测试失败,则须要编写较低级别的测试

二、尽量将测试推到测试金字塔的尽头。

第一条规则很重要,由于较低级别的测试可让你更好地缩小错误并以独立方式复制错误。 当调试手头的问题时,它们会运行得更快,而且不会臃肿。 它们将成为将来良好的回归测试。

第二条规则对于快速保持测试套件很是重要。

若是你已经在较低级别的测试中自信地测试了全部条件,则不须要在测试套件中保留更高级别的测试。 它只是没有增长更多的信心。

多余的测试会在平常工做中变得烦人。

你的测试套件会变慢,当你改变代码的行为时你须要改变动多的测试。

让咱们以不一样的方式来表述:若是更高级别的测试让你更加确信应用程序正常工做,那么你应该拥有它。

为Controller类编写单元测试有助于测试Controller自己的逻辑。

不过,这并不能告诉这个Controller提供的REST端点是否实际响应HTTP请求。 因此,移动测试金字塔并添加一个测试来检查确切的 - 但没有更多。

你不要测试低级测试已经在高级测试中覆盖的全部条件逻辑和边界状况。

确保较高级别的测试侧重于较低级别测试没法覆盖的部分。

当涉及到不提供任何价值的测试时,我很是严格。

我删除了较低级别的高级测试(由于它们不提供额外的值)。

若是可能的话,我用较低级别的测试替换更高级别的测试。

有时候这很难,特别是若是你知道提出一个测试是艰苦的工做。

谨防沉没成本谬误并敲击删除键。

没有理由浪费更多宝贵的时间在再也不提供价值的测试上。

Writing Clean Test Code


就像通常编写代码同样,写出良好和干净的测试代码须要很是细心。

在继续以前提出可维护的测试代码以及在自动化测试套件中破解,如下是一些更多提示:

一、测试代码与生产代码同样重要。 给它一样的关注。 “这只是测试代码”不是证实草率代码合理的理由
二、每一个测试测试一个条件。 这能够帮助你保持测试简短而且容易推理
三、“安排,采起行动,断言”或“当时,那么”是很好的助记符,可让你的测试保持良好的结构
四、可读性很重要。 不要试图过分DRY(Don’t repeat yourself)。 复制是能够的,若是它提升可读性的话。 尝试在DRY和DAMP代码之间找到平衡点
五、若是有疑问,请使用三条规则来决定什么时候重构。 重用以前使用

结论


就这样!我知道这是一个漫长而艰难的阅读,解释为何以及如何测试你的软件。 好消息是,这些信息是持久有用的,而且不管你正在构建什么样样的软件。 不管您是从事微服务领域,物联网设备,移动应用程序仍是Web应用程序,这些文章的教训均可以应用到全部这些领域。

我但愿在这篇文章中有一些有用的东西。

如今继续查看示例代码,并将这里介绍的一些概念加入到您的测试组合中。 有一个坚实的测试组合须要一些努力。

它将会在更长的时间内获得回报,而且会让你的开发者更加安宁,相信我。

Acknowledgements


Thanks to Clare Sudbery, Chris Ford, Martha Rohte, Andrew Jones-Weiss David Swallow, Aiko Klostermann, Bastian Stein, Sebastian Roidl and Birgitta Böckeler for providing feedback and suggestions to early drafts of this article. Thanks to Martin Fowler for his advice, insights and support.

相关文章
相关标签/搜索