单元测试
如果觉得写的还可以,点个赞支持一下笔者呗!你的点赞和关注会让我更快更新哦。笔者会持续更新关于Java和大数据有关的文章。目前集中精力在更新java框架的内容。
1. 为什么要写单元测试
了解软件工程的人都知道,测试是软件开发阶段的最后一环,是保证软件质量的关键步骤(这也是为什么测试人员会被称作 QA 的原因)。只有通过严谨的测试,确保软件达到预期,才能够交付/上线。而单元测试是软件测试中与开发人员关系最紧密的一个环节。
刚刚接触单元测试的时候,看到它的名字我脑海中不由自主的蹦出一个疑问——测试不应该是专门的测试人员做的事情吗,跟开发人员有毛线关系呢?你是不是和我一样有过同样的疑问呢?后来随着对单元测试有了更深入的了解以后,发现写单元测试是每个开发者责无旁贷的事情。
曾经看到过一篇文章写单元测试的七种境界,可以看看自己在哪一层。七种境界大致如下:
- 以各种借口拒绝单元测试
- 尝试单元测试
- 单元测试一切
- 无法忍受脆弱的单元测试
- 发现了一种模拟 mocking 框架,并且乐于使用强制语义
- 模拟 mock 所有可能模拟 mocked 的对象
- 开始真正有效单元测试
通常来说,开发人员对代码进行一次改动,就应该进行一次单元测试。在编写完一个相对独立和完整的功能时,就应该为其编写对应的单元测试代码,而不是等功能开发完再去写单元测试,
不过在我有限的了解中,能够做到单元测试与业务代码同步开发的,真的是凤毛麟角了。能够在写完业务代码就去完成单元测试已经是不错的情况了,更为甚者等功能都上线了,再回过头来去补写单元测试。这种为了写单元测试而写单元测试的做法,完全背离的单元测试的初衷。不过这还不是最糟糕的,还有完全不写单元测试的呢。
单元测试理想的情况是,语句覆盖率可以达到 70% ;核心模块的语句覆盖率和分支覆盖率都要达到 100% 的标准(当年在一家日企工作时,就是这个标准)。
单元测试的意义:
- 对自己代码质量的一种保障
- 为将来的重构保驾护航
- 可以让新人更快熟悉代码
测试金字塔
还记得我们之前讲过的学习金字塔吗?今天我们聊一下另外一个金字塔——测试金字塔:
测试金字塔大致可以分为三层,由下到上分别是:
- 单元测试
- 服务测试
- 用户界面测试
测试金字塔要表达的思想是:自下而上,成本越来越高(右边箭头),效率越来越低(左边箭头)。说得通俗一点就是,往上走事倍功半,往下走事半功倍。有一种说法,这三层的相对收益比(由上到下)为:1 : 2 : 7。但是目前很多公司还在 UI 层面拼命努力着。
结合我们之前讲过的学习金字塔,我们会发现一个非常有意思的现象——大多数人都在收益率最低的那一层拼命努力!想到这里,你有没有觉得后背发凉呢?有没有意识到自己曾经或者正在在某个低收益的层面拼命努力着呢?
2. 一个单元测试的自我修养
身为一个单元测试,要明确自己的定位,时时刻刻都要谨记——自己是一个单元测试。那么我们来看一下具备哪些素质才能称之为一个合格的单元测试呢?
- 有明确的预期
- 可重复运行
- 独立且完整
有明确的预期,这个很好理解,我们的程序输入什么,将会输出什么,这是对于软件的最低要求;
可重复运行也不必多说了,如果我们的单元测试都是一锤子买卖,那也就没有什么写的必要了;
作为一个人,我们要有独立完整的自尊体系,同样,作为一个单元测试也要做到独立且完整。怎么理解呢?
举个例子,假如我们要写一个 Service 层的单元测试,那么运行这个单元测试的时候,就不能去真的访问数据库(因为与数据库打交道是 Dao 层的工作),这叫做独立。虽然不能访问数据库,但是还要保证整个流程可以正确完整的走通(你这不是强人所难吗?),这就是要完整。
那么我们如何做到独立且完整呢?答案就是—— Mock。市面上有很多的 Mock 框架,比如 Mockito、Jmock、easyMock 等。借助这些工具我们可以很轻松的 Mock 出我们想要的依赖。
独立且完整是单元测试的核心思想所在,单元二字将这种思想表达的淋漓尽致。在做单元测试时,我们默认当前功能的所有依赖都是可以正确执行的,我们的目的是测试当前功能的正确性,而这些 Mock 框架可以很好的将被测功能与其他功能隔离开。
3. JUnit
JUnit 是一个 Java 语言的单元测试框架,也是 Spring Boot 默认的单元测试工具,我们先来看一下 JUnit 的几个核心概念和常用注解。
JUnit 核心概念:
JUnit 的相关概念 | 说明 |
Test Class (测试类) | 一个测试类包含一个或多个测试用例 |
Test Case (测试用例) | 一个以 @Test 注释的方法称为 一个测试用例,测试用例必须存在于测试类中。 |
Assert (断言) |
定义想测试的条件,当条件成立时, Assert 方法保持沉默,条件不成立时则抛出异常 |
Suite (测试组) |
关联性紧密的一组测试可以定义为一个 Suite |
Runner (运行器) |
单元测试的运行器(如 SpringRunner),JUnit4 是向后兼容的,可以运行 JUnit3 的测试 |
JUnit 常用注解:
注解 | 说明 |
@RunWith | 指定运行器 |
@BeforeClass | 作用于测试类类加载之前(静态方法) |
@AfterClass | 作用于测试类运行结束时(静态方法) |
@Before | 作用于测试用例运行前 |
@After | 作用于测试用例运行后 |
@Test | 作用于测试用例 |
4. 实战
OK,上面唠叨了那么多,是不是已经手痒了?接下来就来实践一把,过过瘾。要使用单元测试,我们首先需要添加依赖。不过,这次 Spring Boot 好像察觉到了我们的心思,在创建 Web 工程的时候,已经帮我们把 Test
的依赖添加好了,可见 Spring Boot 也是希望我们去写单元测试的。依赖引用如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
在 HelloController
类中按 Command + N
(WIn 下是 Alt + Insert
)或者鼠标右键单击选择 Generate..
然后在弹出的菜单中选择 Test...
:
然后在接下来的对话框按如下图所示,勾选上对于的复选框,然后点击 OK :
然后去 src/test
目录下去找到我们创建的测试类,然后补充剩下的代码:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class HelloControllerTest { @Autowired private MockMvc mvc; @BeforeClass public static void beforeClass() { System.out.println("===before class==="); } @Before public void setUp() throws Exception { System.out.println("===before method==="); } @Test public void hello() throws Exception { MvcResult result = mvc.perform(get("/") .param("name", "IMOOC") .contentType(MediaType.APPLICATION_JSON)) .andExpect(content() .string("Hello IMOOC")) .andReturn(); System.out.println("==="+result.getResponse().getContentAsString()+"==="); } @After public void tearDown() throws Exception { System.out.println("===after method==="); } @AfterClass public static void afterClass() { System.out.println("===after class==="); } }
测试通过的运行效果:
........... ===before class=== 01:16:18.853 [main] DEBUG org.springframework.t test class [com.imooc.springboot.HelloControlle ........... . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.5.RELEASE) 2019-06-17 01:16:19.602 INFO 66981 --- [ ........... 3.474 seconds (JVM running for 4.558) ===before method=== ===Hello IMOOC=== ===after method=== ===after class=== ...........
以上打印的内容进一步验证了上面几个注解的作用。
5. 总结
OK,我们一起学习了一下单元测试相关的内容。知道了 JUnit 的核心概念以及常用注解,并且做了一个 Spring Boot + JUnit 的实例,进一步加深了对于单元测试以及 JUnit 的理解。