这次我们开一个新坑
如何书写测试,测试工作该怎么做,作为一个程序员,测试理论上是开发的一部分,对于当前时代,软件开发会变的更加复杂。如果不进行测试验证,那么代码一方面变得更加复杂,另一方面会出现更多的问题。而实际开发中,很多人不会去写测试用例,是因为觉着麻烦吗,其实更可能因为,广大的程序员不会写测试。
但如果实际去问一些程序员会不会写测试,那么他可能会说,会用xUnit等测试框架。但是真正到了书写的时候,看到自己的代码库。那么就可能出现不知道该如何下手的情况,这种情况可能是代码引入的太复杂导致的,也可以能是当初书写代码的时候并没有很好的分层,预留地址去测试。
同样反过来说。如果代码进行了测试验证,本身就能保证代码的质量,如果不去通过自我测试验证代码,那么自己对代码的自信本身就建立在虚无之上。
那么为了避免这种虚无的自信,我们这一次就来学习测试如何书写测试,
本篇的专栏可以分为
基础,首先是以一个带测试的方式来编写一段代码,对编写测试有一个直观的认识
应用,在后端项目中如何进行测试,以Spring为例,进行测试
扩展,讲解TDD和BDD两项开发实战。
那么第一篇,我们先看如何实现一个带测试应用。在此过程中,讲解如何设计测试场景,并且如何将测试场景转换为测试用例,并且如何同时进行测试和正常开发两方面的书写。
我们这一次实现一个简单的应用
这个应用主要包含的功能有添加一个对象到存储,标记对象为Done,查看对象列表(可以分别查看Done和非Done的对象)
查看所有的对象列表
在书写完成功能之后,利用相关的组件将其进行命令行化。
那么我们就需要一个实体,Todo项
可以顺应三个动作 addTodoItem 添加Todo项
markTodoItemDone 标记为Done,list 列出所有的Todo项。
那么我们就进行相关的书写
首先是add新增一个Item
这里我们声明一个interface
TodoItem addTodoItem(final TodoParameter todoParameter);
在这个接口中,我们需要考虑传入的Parameter中如果有null值,该怎么办
不过在一般开发中,往往这些校验是在Parameter创建的时候校验的,这里我们完全可以不进行校验。
那么首先我们就可以针对这样的一个接口,编写相关的测试用例,比如如下
@Test public void should_add_todo_item() { TodoItemRepository repository = mock(TodoItemRepository.class); when(repository.save(any())).then(returnsFirstArg()); TodoItemService service = new TodoItemService(repository);
TodoItem item = service.addTodoItem(new TodoParameter(“foo”));
assertThat(item.getContent()).isEqualTo(“foo”); } |
这里我们通过自我组装的方式。创建出了TodoItemRepository
确认save之后的item是否只有foo
这里利用了assertThat来进行判断
并且利用save操作之后.then来模拟动作进行返回。
对应的service代码为
public TodoItem addTodoItem(final TodoParameter todoParameter) { final TodoItem item = new TodoItem(todoParameter.getContent()); return this.repository.save(item); } |
接下来我们进行下一项的测试,这里测试的重点是空对象。
也即是传入的pararm是null
public TodoItem addTodoItem(final TodoParameter todoParameter) { if (todoParameter == null) { throw new IllegalArgumentException(“Null or empty content is not allowed”); } final TodoItem item = new TodoItem(todoParameter.getContent()); return this.repository.save(item); } |
同样,对应的Test代码则是
@Test public void should_throw_exception_for_null_todo_item() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> service.addTodoItem(null)); } |
那么同理,最后的一个操作标记为Done也顺理成章了。
TodoItem markTodoItemDone(TodoIndexParameter index);
不过需要考虑,对于索引超过当前已经现存的索引范围,返回空即可。
同样finalAll方法的定义如下
List<TodoItem> list(final boolean all);
当all为True的时候,列出所有Todo,为False的时候,列出所有未完成项
最后我们给出item实体的定义
@Getter public class TodoItem { private long index; private final String content; private boolean done;
public TodoItem(final String content) { this.content = content; this.done = false; }
public void assignIndex(final long index) { this.index = index; }
public void markDone() { this.done = true; } } |
从上面可以看出来,我们在编写代码的同时,编写了测试用例。
那么从上面的过程,我们可以总结一下
如果要针对一个需求编写一段代码
我们可以先根据需求分解任务
然后设计一个可以测试的函数
针对具体的函数,考虑测试场景,具现化为测试用例。
那么接下来,我们就实现具体的核心仓库代码,并利用相关组件进行封装,封装为命令行格式。并实现相关测试。
首先是Todo的存储,我们预留了接口,只需要实现Repository 这个接口。
这个接口内的函数主要有
public interface TodoItemRepository {
TodoItem save(TodoItem item);
Iterable<TodoItem> findAll();
}
这里我们需要设置相关的边界项
比如查询空的Repository,返回空列表
保存后再查询,返回一个保存之后的列表
保存空的对象,会抛出异常。
那么我们先可以拿文件作为实际的存储,来给Repository增强能力。
对于存储的位置,则是可以作为仓库类初始化的时候一个参数进行传入。
那么在测试的就是,可以利用Junit中的@TempDir获取一个临时目录进行传入。
那么我们在配合Jackson作为编解码的工具,进行读取和存入。
对于实际的代码,则是如下
@Override public Iterable<TodoItem> findAll() { if (this.file.length() == 0) { return ImmutableList.of(); }
try { final CollectionType type = typeFactory.constructCollectionType(List.class, TodoItem.class); return mapper.readValue(this.file, type); } catch (IOException e) { throw new TodoException(“Fail to read todo items”, e); } } |
然后我们在进行测试的时候,会发现上面的代码里,会有部分的代码无法覆盖到。
对于这里的问题,可以通过强行出现Exception进行验证,也可以将constructCollectionType抽取出来,从而进行测试。
而对于抽取出的Util类,则可以通过构造脚本的排除,将其排除在覆盖测试之外。
毕竟我们的目的是测试自己的代码,而不是他人的程序库。
对于命令行的集成,我们可以选择Picocli,这里就不详细讲了。
最后是进行集成测试
对于集成测试,这里需要注意几点,首先是对于集成测试中,关于对象的组装,如果可以使用DI容器完成,就使用相关容器。
如果不行,则封装一个合适的方法去创建。
public class ObjectFactory { public CommandLine createCommandLine(final File repositoryFile) { return new CommandLine(createTodoCommand(repositoryFile)); }
private TodoCommand createTodoCommand(final File repositoryFile) { final TodoItemService service = createService(repositoryFile); return new TodoCommand(service); }
public TodoItemService createService(final File repositoryFile) { final TodoItemRepository repository = new FileTodoItemRepository(repositoryFile); return new TodoItemService(repository); } } |
然后是在集成测试的时候,将之前测试好的用例作为一个稳定项,再次基础上增加未测试的功能。
@Test public void should_mark_as_done() { service.addTodoItem(TodoParameter.of(“foo”));
cli.execute(“done”, “1”);
final List<TodoItem> items = service.list(true); assertThat(items.get(0).isDone()).isTrue(); } |
那么总结一下。
我们先说明了如何利用文件完成仓库相关代码。
并且说明了在测试的时候完全可以将第三方的代码和自己的代码进行分离。自己的代码有覆盖即可。
最后就是在集成测试的时候,将已经测试好的稳定组件作为基础,在此基础上进行测试。