最近,我的一些学生向我询问了赫尔辛基大学MOOC提供的单元测试的机制,我检查了它们的实现,并认为这对于初学者了解实际发生的情况是有帮助的,因此在此发表了这篇小文章。
我们将以“机场”项目为例,这是OOP2第一周的最后一项任务。
我们仅关注测试,因此我将跳过有关如何解决它的事情。 在本练习中,我们将每次手动执行main
方法,重复输入飞机编号,容量,有时我们认为我们的代码可以工作,然后运行本地测试,以便可以提交给服务器进行在线判断和评分。
我一直使用这个小项目作为借助单元测试保护的重构示例。 当我又重复又痛苦地输入飞机ID,航班号,机场代码和操作代码时,我问我的学生:“这很痛苦吗?”。
显然,他们所有人都回答了。 然后我问,“即使无聊又痛苦,您会一次又一次地进行这种测试吗?”
安静。
从我过去的经验中,我知道跳过这些无聊的测试很容易,并且我们可以安慰自己,“这些代码非常简单,我不会犯错,它将起作用并且会起作用,不用担心。”
由于做出这样的选择,我会留下痛苦的回忆,因为过去我犯了太多简单而愚蠢的错误,所以无论看起来多么简单,我仍然会进行测试-即使是手动测试,也无聊而痛苦。
我添加此内容是因为单元测试无法完全替代手动测试,尽管它将使手动测试更加容易和有效。
对于Airport项目,如果不需要每次都重复输入,并且可以捕获程序的输出,则与预期相比,我们将更快地获得反馈。
String operation = scanner.nextLine();
...
System.out.println("Blahblahblah...");
例如,我们确切地知道是否首先输入x
,然后它将进入飞行服务部分并打印菜单选项;如果我们第二次输入x
,则程序将结束循环并退出,结果,我们将仅获取机场面板和飞行服务的说明输出。
因此,让我们转到一个测试用例,看看实际会发生什么。
@Test
public void printsMenusAndExits() throws Throwable {String syote = "x\nx\n";MockInOut io = new MockInOut(syote);suorita(f(syote));String[] menuRivit = {"Airport panel","[1] Add airplane","[2] Add flight","[x] Exit","Flight service","[1] Print planes","[2] Print flights","[3] Print plane info","[x] Quit"};String output = io.getOutput();String op = output;for (String menuRivi : menuRivit) {int ind = op.indexOf(menuRivi);assertRight(menuRivi, syote, output, ind > -1);op = op.substring(ind + 1);}
}
上面是第二个测试用例,它涵盖了我们所说的最简单的情况,仅输入两个x
。
当我们查看测试代码时,它分为三部分:
- 准备输入
- 执行
Main.main(args)
方法 - 检查输出以查看它是否依次包含所有预期行
您知道scanner.nextLine()
或scanner.nextInt()
的正常行为。 该程序将挂起并等待用户输入,以便执行下一行代码。 但是,为什么它在这里没有任何等待就可以顺利运行?
在开始这一部分之前,我想简要解释一下该方法的执行,它使用Java反射以一种不直接但可以进行更多检查的方式来调用该方法,例如,第一个测试用例要求Main
为公共类,但您可能会发现要通过手动测试,可以将Main
访问级别设置为package。
@Test
public void classIsPublic() {assertTrue("Class " + klassName + " should be public, so it must be defined as\n" +"public class " + klassName + " {...\n}", klass.isPublic());
}
这里klass.isPublic()
正在检查是否根据需要设置访问级别。
好。 看来MockInOut
类使魔术发生了,我们可以检查代码以在MockInOut
找到想法。 您可以在GitHub上访问源代码。
public MockInOut(String input) {orig = System.out;irig = System.in;os = new ByteArrayOutputStream();try {System.setOut(new PrintStream(os, false, charset.name()));} catch (UnsupportedEncodingException ex) {throw new RuntimeException(ex);}is = new ByteArrayInputStream(input.getBytes());System.setIn(is);
}
您可能已经输入System.out
数千次,但是您是否意识到可以像上面一样默默地更改out
? 这同时设置out
与in
系统的,这样我们就可以完全执行后得到的输出,我们也不需要手工输入这个时候,因为在声明Scanner scanner = new Scanner(System.in);
,则参数System.in
会以无提示方式更改,因此scanner.nextLine()
将获得准备好的输入而不会挂起。
同样,输出将不会在控制台中打印,而是会累积到ByteArrayOutputStream
,此后可以访问。
您可能想知道,如果我们真的要恢复System.in
和System.out
的正常行为,该怎么办?
/*** Restores System.in and System.out*/
public void close() {os = null;is = null;System.setOut(orig);System.setIn(irig);
}
基本上,它节省了原来in
和out
,需要恢复时,只需再次清除遭入侵的人,并设置他们回来,那么一切都将照常进行。
您可以在下面复制简单的示例代码以进行快速测试。
import java.io.*;
import java.util.*;class HelloWorld {public static void main(String[] args) throws IOException {PrintStream orig = System.out;ByteArrayOutputStream os = new ByteArrayOutputStream();System.setOut(new PrintStream(os, false, "UTF-8"));// Here it won't print but just accumulatefor (int i = 0; i < 100; i++) {System.out.println("Hello World");}System.setOut(orig);// Print 100 lines of "Hello World" here since out was restoredSystem.out.println(os.toString("UTF-8"));InputStream is = System.in;System.setIn(new ByteArrayInputStream("x\nx\n".getBytes()));Scanner scanner = new Scanner(System.in);// Without hang onSystem.out.println(scanner.nextLine());System.out.println(scanner.nextLine());try {// There are only two lines provided, so here will failSystem.out.println(scanner.nextLine());} catch (NoSuchElementException e) {e.printStackTrace();}System.setIn(is);scanner = new Scanner(System.in);// Hang on here since `in` was restoredSystem.out.println(scanner.nextLine());}
}
实际上,注入和替换是一种用于分离单元测试依赖关系的常用方法,这对于仅关注代码非常有用。 还有更先进,更复杂的方法来做到这一点,但在这里,我们只是想说明一个简单的方法是“黑客” in
和out
,这样你可以专注于你的代码,而不是in
与out
。
对于某些遗留项目,此方法可能对重构至关重要,因为太多的依赖关系使测试变得非常困难!
翻译自: https://www.javacodegeeks.com/2019/02/approach-simulate-input-check-output.html