当前位置: 首页 > 科技观察

编写更好的Java单元测试的七个技巧

时间:2023-03-22 14:52:14 科技观察

测试是开发中非常重要的一个方面,可以在很大程度上决定应用程序的命运。良好的测试可以及早发现导致应用程序崩溃的问题,但糟糕的测试往往会导致故障和停机。虽然软件测试主要分为三种类型:单元测试、功能测试和集成测试,但在这篇博文中,我们将讨论开发人员级别的单元测试。在我进入细节之前,让我们回顾一下这三个测试中每一个的细节。软件开发测试的类型单元测试用于测试单个代码组件并确保代码按预期工作。单元测试由开发人员编写和执行。大多数时候,使用的是JUnit或TestNG等测试框架。测试用例通常在方法级别编写并通过自动化执行。集成测试检查系统是否作为一个整体工作。集成测试也是由开发人员完成的,但它不是测试单个组件,而是旨在跨组件进行测试。一个系统由许多单独的组件组成,例如代码、数据库、Web服务器等。集成测试可以发现组件布线、网络访问、数据库问题等问题。功能测试通过将给定输入的结果与规范进行比较来检查每个功能是否正确实现。通常,这不在开发人员级别。功能测试由单独的测试团队执行。测试用例是根据规范编写的,并将实际结果与预期结果进行比较。有多种工具可用于自动化功能测试,例如Selenium和QTP。如前所述,单元测试帮助开发人员确定代码是否正常工作。在这篇博文中,我将提供有关Java单元测试的有用技巧。1.使用框架进行单元测试Java提供了几种单元测试框架。TestNG和JUnit是最好的测试框架。JUnit和TestNG的一些重要特性:易于设置和运行。支持评论。允许某些测试被忽略或组合在一起执行。支持参数化测试,即在运行时通过指定不同的值来运行单元测试。通过与Ant、Maven和Gradle等构建工具集成,支持自动化测试执行。EasyMock是一个模拟框架,可以补充JUnit和TestNG等单元测试框架。EasyMock本身并不是一个完整的框架。它只是增加了创建模拟对象的能力,以便于测试。例如,我们要测试的一种方法可以调用从数据库中获取数据的DAO类。在这种情况下,EasyMock可用于创建返回硬编码数据的MockDAO。这使我们能够轻松地测试我们想要的方法,而不必担心数据库访问。2.谨慎使用测试驱动开发!测试驱动开发(TDD)是一种软件开发过程,我们在开始任何编码之前根据需求编写测试。测试最初会失败,因为还没有代码。然后编写最少量的代码来通过测试。然后重构代码,直到它被优化。目标是编写涵盖所有需求的测试,而不是一开始就编写可能甚至无法满足需求的代码。TDD很棒,因为它导致了易于维护的简单模块化代码。整体开发速度加快,缺陷容易发现。此外,单元测试是作为TDD方法的副产品创建的。然而,TDD可能并不适合所有情况。在具有复杂设计的项目中,如果不提前考虑就专注于最简单的设计以通过测试用例,可能会导致巨大的代码更改。此外,TDD方法对于与遗留系统、GUI应用程序或使用数据库的应用程序交互的系统来说很困难。此外,测试需要随着代码的更改而更新。因此,在决定采用TDD方法之前应考虑上述因素,并根据项目的性质采取措施。3.衡量代码覆盖率代码覆盖率衡量(以百分比表示)运行单元测试时执行的代码量。一般来说,具有高覆盖率的代码包含未检测到的错误的可能性较低,因为它的更多源代码是在测试期间执行的。衡量代码覆盖率的一些最佳实践包括:使用代码覆盖率工具,例如Clover、Corbetura、JaCoCo或Sonar。使用工具可以提高测试质量,因为这些工具可以指出未测试的代码区域,允许您开发额外的测试来覆盖这些区域。每当编写新功能时,立即编写新的测试覆盖率。确保测试用例覆盖代码的所有分支,即if/else语句。高代码覆盖率并不能保证测试是完美的,所以要小心!以下concat方法接受一个布尔值作为输入,并仅在布尔值为真时附加两个字符串:publicStringconcat(booleanappend,Stringa,Stringb){Stringresult=null;If(append){result=a+b;}returnresult.toLowerCase();}这是上述方法的测试用例:@TestpublicvoidtestStringUtil(){Stringresult=stringUtil.concat(true,"Hello","World");System.out.println("Resultis"+result);在这种情况下,执行值为true的测试。当测试执行时,它将通过。当代码覆盖率工具运行时,它会显示100%的代码覆盖率,因为concat方法中的所有代码都被执行了。但是,如果测试执行评估为false,则会抛出NullPointerException。所以100%的代码覆盖率并不意味着测试覆盖了所有场景,也不意味着测试就很好。4.尽可能将测试数据外部化在JUnit4之前,测试用例要运行的数据必须硬编码到测试用例中。这导致了限制,即为了使用不同的数据运行测试,必须修改测试用例代码。但是,JUnit4和TestNG都支持外部化测试数据,因此可以在不更改源代码的情况下针对不同的数据集运行测试用例。以下MathChecker类具有检查数字是否为奇数的方法:publicclassMathChecker{publicBooleanisOdd(intn){if(n%2!=0){returntrue;}else{returnfalse;}}}这是MathChecker的TestNG测试用例类:publicclassMathCheckerTest{privateMathCheckerchecker;@BeforeMethodpublicvoidbeforeMethod(){checker=newMathChecker();}@Test@Parameters("num")publicvoidisOdd(intnum){System.out.println("Runningtestfor"+num);Booleanresult=checker。isOdd(num);Assert.assertEquals(result,newBool??ean(true));}}TestNG这是testng.xml(TestNG的配置文件),其中包含执行测试的数据:可以看出,在这种情况下测试会执行两次,一次为值3和7。除了通过XML配置文件指定测试数据外,还可以通过DataProvider注解在类中提供测试数据。JUnit与TestNG相似,因为测试数据也可以外部化以用于JUnit。这是与上面相同的MathChecker类的JUnit测试用例:@RunWith(Parameterized.class)publicclassMathCheckerTest{privateinputNumber;privateBooleanexpected;privateMathCheckermathChecker;@Beforepublicvoidsetup(){mathChecker=newMathChecker();}//InjectviaNestexpedumlepublicMathCheckerinputNumber=inputNumber;this.expected=expected;}@Parameterized.ParameterspublicstaticCollectiongetTestData(){returnArrays.asList(newObject[][]{{1,true},{2,false},{3,true},{4,false},{5,true}});}@TestpublicvoidtestisOdd(){System.out.println("Runningtestfor:"+inputNumber);assertEquals(mathChecker.isOdd(inputNumber),expected);}}可以看出,要执行测试的测试数据由getTestData()方法指定。可以轻松修改此方法以从外部文件读取数据,而不是对数据进行硬编码。5.使用断言代替Print语句许多新手开发人员习惯于在每行代码之后编写System.out.println语句来验证代码是否正确执行。这种做法经常扩展到单元测试,导致测试代码混乱。除了混乱之外,这还需要开发人员手动干预以验证打印到控制台的输出以检查测试是否成功运行。更好的方法是使用自动指示测试结果的断言。下面的StringUti类是一个简单的类,它有一个连接两个输入字符串并返回结果的方法:publicclassStringUtil{publicStringconcat(Stringa,Stringb){returna+b;}}下面是对上述方法的两个单元测试:@TestpublicvoidtestStringUtil_Bad(){Stringresult=stringUtil.concat("Hello","World");System.out.println("Resultis"+result);}@TestpublicvoidtestStringUtil_Good(){Stringresult=stringUtil.concat("Hello","World");assertEquals("HelloWorld",result);}testStringUtil\_Bad将始终通过,因为它没有断言。开发者需要在控制台手动验证测试的输出。如果方法返回不正确的结果并且不需要开发人员干预,testStringUtil\_Good将失败。6.构建具有确定性结果的测试有些方法没有确定性结果,即该方法的输出是事先不知道的,每次都可能改变。例如,考虑以下代码,它有一个复杂函数和一个计算执行该复杂函数所需时间(以毫秒为单位)的方法:int)(Math.random()*100);Thread.sleep(time);}catch(InterruptedExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}}publiclongcalculateTime(){longtime=0;longbefore=System.currentTimeMillis();veryComplexFunction();longafter=System.currentTimeMillis();time=after-before;returntime;}}这种情况下,每次执行calculateTime方法,都会返回不同的值。为该方法编写测试用例不会有任何好处,因为该方法的输出是可变的。因此,测试方法将无法验证任何特定执行的输出。7.测试正面场景之外的负面场景和边缘案例通常,开发人员会花费大量时间和精力编写测试用例,以确保应用程序按预期工作。但是,测试负面测试用例也很重要。否定测试用例是指测试系统是否可以处理无效数据的测试用例。例如,考虑一个简单的函数,它读取用户键入的长度为8的字母数字值。除字母数字值外,还应测试以下否定测试用例:用户指定非字母数字值,例如特殊字符。用户指定的空值。用户指定一个大于或小于8个字符的值。同样,边界测试用例测试系统对极值的适用性。例如,如果用户希望输入一个从1到100的数值,那么1和100就是边界值,测试这些值的系统非常重要。翻译链接:http://www.codeceo.com/article/7-trends-java-unit-test.html英文原文:7TipsforWritingBetterUnitTestsinJava