写测试相比于写代码来说算是一件简单的事。多数时候,我们并不需要考虑复杂的逻辑。我们只需要按照我们的代码逻辑,对代码的行为进行覆盖。
需要注意的是——在不同的团队、工作流里,测试可能是会有不同的工作流程:
在允许的情况下,测试应该由开发人员来编写,并且是由底层开始写测试。为了更好地去测试代码,我们需要了解测试金字塔。
###测试金字塔
测试金字塔是由 Mike Cohn 提出的,主要观点是:底层单元测试应多于依赖 GUI 的高层端到端测试。其结构图如下所示:
从结构上来说,上面的金字塔可以分成三部分:
从图中我们可以发现:单元测试应该要是最多的,也是最底层的。其次才是服务测试,最后才是 UI 测试。大量的单元测试可以保证我们的基础函数是正常、正确工作的。而服务测试则是一门很有学问的测试,不仅仅只在测试我们自己提供的服务,也会测试我们依赖第三方提供的服务。在测试第三方提供的服务时,这就会变成一件有意思的事了。除此还有对功能和 UI 的测试,写这些测试可以减轻测试人员的工作量——毕竟这些工作量转向了开发人员来完成。
####单元测试
单元测试是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。它是应用的最小可测试部件。举个例子来说,下面是一个JavaScript 的函数,用于判断一个变量是否是一个对象:
var isObject = function (obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
这是一个很简单的功能,对应的我们会有一个简单的 Jasmine 测试来保证这个函数是正常工作的:
it("should be a object", function () {
expect(l.isObject([])).toEqual(true);
expect(l.isObject([{}])).toEqual(true);
});
虽然这个测试看上去很简单,但是大量的基本的单元测试可以保证我们调用的函数都是可以正常工作的。这也相当于是我们在建设金字塔时用的石块——如果我们的石块都是经常测试的,那么我们就不怕金字塔因为石块的损坏而坍塌。
当单元测试达到一定的覆盖率,我们的代码就会变得更健壮。因为我们都需要保证我们的代码都是可测的,也意味着我们代码间的耦合度会降低。我们需要去考虑代码的长度,越长的代码在测试的时间会变得越困难。这也就是为什么 TDD 会促使我们写出短的代码。如果我们的代码都是有测试的,单元测试可以帮助我们在未来重构我们的代码。
并且在很多没有文档或者文档不完整的开源项目中,了解这个项目某个函数的用法就是查看他的测试用例。测试用例(Test Case)是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。这些测试用例可以让我们直观地理解程序程序的 API。
####服务测试
服务测试顾名思义便是对服务进行测试,而服务可以是有不同的类型,不同层次的测试。如第三方的 API 服务、我们程序提供的服务,虽然他们他应该在这一个层级上进行测试,但是对他们的测试会稍有不同。
对于第三方的提供的 API 服务或者其他类似的服务,在这一个层级的测试,我们都不会真实地去测试他们能不能工作——这些依赖性的服务只会在功能测试上进行测试。在这里的测试,我们只会保证我们的功能代码是可以正常工作的,所以我们会使用一些虚假的 API 测试数据来进行测试。这一类提供 API 的 Mock Server 可以模拟被测系统外部依赖模块行为的通用服务。我们只要保证我们的功能代码是正常工作的,那么依赖他的服务也会是正常工作的。
而对于我们提供的服务来说,这一类的服务不一定是 API 的服务,还有可能是多个函数组成的功能性服务。当我们在测试这些服务的时候,实际上是在测试这个函数结合在一起是不是正常的。
一个服务可能依赖于多个函数,因而我们会发现服务测试的数量是少于单元测试的。
####UI 测试
在传统的软件开发中,UI 测试多数是由人手动来完成的。而在稍后的章节里,你将会看到这些工作是可以由机器自己来完成的——当然,前提是我们要编写这些自动化测试的代码。需要注意的是 UI 测试并不能完全替代手工的工作,一些测试还是应该由人来进行测试——如对 UI 的布局,在现阶段机器还没有审美意识呢。
自动化 UI 测试是一个缓慢的过程,在这个过程里我们需要做这么几件事:
所以,你会发现这是一个很长的测试过程。尽可能地将这个层级的测试往下层级移,就会尽可能的节省时间。一个 UI 测试需要几分钟,但是一个单元测试可能不到1秒。这就意味着,这样的测试下移可以节省上百个数量级的时间。
###如何测试
现在问题来了,我们应该怎么去写测试?换句话来说,我要测什么?这是一个很难的问题,这足够可以以一本书的幅度来说明这个问题。这个问题也需要依赖于不同的实践,不同的时候我们可能对问题的看法都有不同。
编写测试的过程大致可以分成下面的几个步骤:
####测试目的
我们在上面提到过的测试金字塔,也表明了我们在每个层级要测试的目的是不一样的。
在单元测试这一层级,因为我们所测试的是每一个函数,这些函数没有办法构成完整的功能。这时候我们就只是用于简简单单的测试函数本身的功能,没有太多的业务需求。
而对于服务这一层级,我们所要测试的就是一个完整的功能。对于以 API 为主的项目来说,实际上就是在测返回结果是否是正确的。
最后 UI 这一层级,我们所需要测试的就是一个完整的功能。用户操作的时候应该是怎样的,那么我们就应该模仿用户的行为来测试。这是一个完整的业务需求,也可以称之为验证测试。
####测试点
在了解完我们要测试的目的之后,我们要测试的点也变得很清晰。即在单元测试测试我们的函数的功能,在我们的服务测试我们的服务,在我们的 UI测试测试业务。
而这些都理想的情况,当系统由于业务的原因不得不耦合的时候。究竟是单元测试还是功能测试,这是一个特别值得思考的问题。如果一个功能既可以在单元测试里测,又可以在服务测试里测,那么我们要测试哪一个?或者说我们应该把两个都测一遍?而如果是花费时间更长的 UI 测试呢?这样做是不是会变得不划算。
####如何写测试代码
先让来们来简单地看一下测试用例,然后再让我们看看一般情况下我们是如何写测试代码的。下面的代码是一个用Python写的测试用例:
class HomepageTestCase(LiveServerTestCase):
def setUp(self):
self.selenium = webdriver.Firefox()
self.selenium.maximize_window()
super(HomepageTestCase, self).setUp()
def tearDown(self):
self.selenium.quit()
super(HomepageTestCase, self).tearDown()
def test_can_visit_homepage(self):
self.selenium.get(
'%s%s' % (self.live_server_url, "/")
)
self.assertIn("Welcome to my blog", self.selenium.title)
在上面的代码里主要有三个方法,setUp()、tearDown()和 test_can_visit_homepage()。在这三个方法中起主要作用的是 test_can_visit_homepage()方法。而 setUp() 和 tearDown() 是特殊的方法,分别在测试方法开始之前运行和之后运行。同时,在这里我们也用这两个方法来打开和关闭浏览器。
而在我们的测试方法 test_can_visit_homepage() 里,主要有两个步骤:
大部分的测试代码也是以如何的流程来运行着。有一点需要注意的是:一般来说函数名就表示了这个测试所要做测试的事情,如这里就是测试可以访问首页。
如上所示的测试过程称为“四阶段测试”,即这个过程分为如下的四个阶段:
####Tips
需要注意的几点是:
参考书籍: