51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 1875|回复: 2
打印 上一主题 下一主题

[讨论] 聊聊如何写单元测试

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2018-2-27 15:09:15 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
这篇文章的假设为你明确自己要写单元测试了,如果您不符合这个假设,可以参看这篇文章 先解决
思想:为何要写单元测试

当打算开始写单元测试时。你调整了下坐姿,气运丹田,感觉到冥冥之中又向高质量代码迈进了一
步,但当你的手下意识的敲击键盘的时候,又觉得似乎哪里不太对劲:“恩,应该怎样开始写一个单
元测试呢?”

如何写单元测试

首先我们需要明确,什么叫做单元测试。

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的
最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。
我的理解是:测试某个具体的函数,是否符合编写者的预期。

其实也很好理解,就是将你编写某个函数的功能与你的预期做一个比较,如果函数运行的结果与你
的预期相符,则说明测试通过,反之则失败。

举个很简单的例子
  1. class Person
  2.   attr_accessor :name, :gender

  3.   def initialize(name, gender)
  4.     self.name = name
  5.     self.gender = gender
  6.   end

  7.   def say_hello
  8.       puts "#{self.title} #{name} said hello."
  9.   end

  10.   def title
  11.      self.gender == "male" ? "Mr" : "Ms"
  12.   end
  13. end
复制代码
我想测试一下 title 这个函数是否符合我预期,于是我会这样写测试(假设使用Rails 原生的 test框架)
  1. require 'test_helper'

  2. class PersonTest < ActiveSupport::TestCase
  3.   test "should return title correctly" do
  4.     person = Person.new("Ji Cheng", "Male")
  5.     assert_equal "Mr", person.title

  6.     person = Person.new "Han Meimei", "Female"
  7.     assert_equal "Ms", person.title
  8.   end
  9. end
复制代码
语言不同,测试框架不同都会导致代码不同,但是思想都是一样的,都是去 assert 一个值,与运行
后的函数值保持一致。

也许有人会说你这个函数太简单了,简直不用看就知道会发生什么,为什么还要写个测试? 其实想
想,写出这个测试也花多长时间,更重要的是,你在写这个测试的时候,会更加清楚这个函数输入
与输出,是否满足预期,以及多一次使用自己写的函数的机会,体谅下调用你写的函数的人。

细心的朋友肯定已经看出来了,这个测试是会报错的,如果你真没看出来,就更加说明单元测试的
重要性。实际情况中太多函数是看不出来的,但是例如下面的 analysis_message
  1. class JobStepService
  2.   attr_accessor :project, :job, :flow

  3.   # Some functions ...

  4.   class << self
  5.     def analysis_message(hash)
  6.       job_id = hash[:job_id]
  7.       index = hash[:index]
  8.       job_step = JobStep.find_by(job_id: job_id, index: index)
  9.       return if job_step.nil? || job_step.status == "stopped"

  10.       try_mark_last_job_status(job_id, index)
  11.       where = JobStep.where(job_id: job_id, index: index)
  12.       safe_update_job_hash(where, hash)
  13.       job_step.reload
  14.     end

  15.     private

  16.     def try_mark_last_job_status(job_id, index)
  17.       return if index.to_i.zero?
  18.       # 必须得找到, 不找到肯定是哪里出错了,应该抛异常
  19.       JobStep.find_by(job_id: job_id, index: index.to_i - 1).update_attribute(:status, "success")
  20.     end
  21.   end
  22. end
  23. 当不是那么容易看出的时候,去写一个单元测试是跟你在命令端调试所花的时间是差不
复制代码





想必大家也看出来了,测试甚至有些随意不太友好,但是至少在跑了这段测试之后,我很信任之前
写的函数是没有问题的(就算有,也不会是那些会被同事耻笑的低级错误)。

写单元测试一些实践

大前提

所有的实践前面都有一个大前提:首先你得写单元测试。我非常喜欢写一些显而易见的单元测试当
做休息放松,当别人问我为什么写这种测试的时我通常是以“增加代码测试覆盖率”来忽悠他们。
(但是实际上还是有30%左右的概率会测出各种问题,包含各种语法错误,误触某回调等奇怪的错
误校验不过,也许我就是一个粗心的人),这样做还有另外一个好处,培养自己对每个方法都写测
试的习惯:连很简单的方法都写了,那稍微复杂点的,简直不能忍。

谁来写

开发来写。单测主要测试的是具体的函数,没有比开发人员更熟悉自己写的函数了,同时本着“吃
自己的狗粮”的原则,也可以反省下自己设计的函数是否合理。最重要的,当自己写完一个的时候,
就可以把单元测试当做自己手动调试代码,这样就可以很自然的无缝的将单测写上,而不用等测
试人员排队做。

关于测试覆盖率

虽然这个东西听起来很虚,但我觉得是个必需品,必须得上。当有一个标准去衡量自己的工作进
度的时候,潜意识中大家会努力的提高这个指标。同时绝大多数测试覆盖率统计工具,都能通过
界面显示出你函数中未覆盖的逻辑,避免自己漏测。我自己使用simplecov 这个gem 来统计我自
己的Rails 项目的测试覆盖。

写的测试跑着要快

我非常赞同,写的测试越慢,由于人的惰性,会导致自己因为不想等太久而不跑测试。测试写的
再多,不跑全是白搭。

其实一个单元测试的内容很少,那么一般慢是慢在哪里呢?

我觉得有以下方面

IO
sleep/wait 语句
数据库的大量写入
关于IO
目前我遇见比较多的是关于网络的IO, 有些第三方组件会接入网络,这种一般都会带来500ms

左右的延时,运气不好连国外(比如我们的项目连github API)没准就会变成假摔(一定概率的
跑出错,非必现的错误)。常见的操作是 Stub 解决问题,各大语言都有很成熟的解决方案。比
如我现在使用的 webmock 这个 gem ,当然以ruby 这种 “开放式” 语言的能力,就算不引入任何
gem,写个猴子补丁也会非常的轻松。

关于sleep/ wait
大多数使用sleep/ wait 的时候都是在等待某个异步方法的执行完成,我的处理方式是将异步的处
理以及等待后面的语句都抽成两个独立的函数,分别测试这两个函数,从而避免走sleep 这种慢
的操作

关于数据库
很多测试相关的文章和书籍都强调 数据库太慢了,所以不能使用数据库。我不太认同,因为其
实很多时候写的代码都需要依赖数据库的一些特性,或者离开数据库而存在内存中会很麻烦(
比如查询语句,脱离数据库mock 个 where 很麻烦)。我的策略(当然是Rails 的策略)是使用
专门用于测试的数据库,每当运行一个单侧的时候就会把它清除掉。这样,测试数据库的数据
会非常的少,查询、新增起来大多数情况下其实也在20ms以内。

我是非常反对当一个单元测试跑完后,不清除数据库的,可能这些数据会影响到其他单元测试,
进一步造成了测试的假摔,假摔是大忌,应该尽量避免。当然清数据库也不是绝对的,需要自
己灵活判别,比如下面的情况。

我运行测试之前会生成100条左右的模板数据,这些数据是我在进行单元测试的时候绝对不会
操作的,所以没必要每次执行一个单元测试删除再新建。但是为了防止我自己有时候没想清楚
改掉模板数据从而有可能造成假摔,所以我在执行每个单元测试之前会判断下这些模板数据的
行数是否是我最初的生成的行数。

单元测试不是万能的

会有人觉得我花了那么大的功夫,覆盖率90%了,上 jenkins 或者 flow.ci 了,那我的程序就
很稳定了。这种观念当然是不对的,就如同你买了一把200块的锁就指望自己的自行车永远
不会被偷一样。良好的单元测试会极大的提高程序稳定性,但是不会百分百的保证程序一
定ok,毕竟人无完人。

从入门到放弃?

相信很多朋友其实也写过单测,但或因需求变更过快,或因一次次的失败无力解决,导致了
最终没有坚持下来。这当中其实是有一定技巧的,使用良好的技巧会在保证测试覆盖率的
同时,降低测试失败的频率。下次就来说说 如何使用一些技巧,让我们容易坚持执行这个
应该坚持的单元测试。

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏
回复

使用道具 举报

  • TA的每日心情
    奋斗
    4 小时前
  • 签到天数: 1799 天

    连续签到: 3 天

    [LV.Master]测试大本营

    2#
    发表于 2018-2-27 15:40:03 | 只看该作者
    谢谢分享,对单元测试有了一个更深入的了解
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-13 14:16 , Processed in 0.068658 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表