什么是单元测试
单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。
程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和期望的一致。
为什么要使用单元测试
如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。
但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确。
编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。
单元测试的优点
1、它是一种验证行为。
程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。
2、它是一种设计行为。
编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
3、它是一种编写文档的行为。
单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
4、它具有回归性。
自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
单元测试所要做的工作
1、它的行为和我期望的一致吗?
这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的
2、它的行为一直和我期望的一致吗?
编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一个项复杂的工程,在测试某段代码的行为是否和你的期望一致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。
3、 我可以依赖单元测试吗?
不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。
4、单元测试说明我的意图了吗?
单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。
单元测试的覆盖种类
语句覆盖
语句覆盖就是设计若干个测试用例,运行被测试程序,使得每一条可执行语句至少执行一次
判定覆盖(也叫分支覆盖)
设计若干个测试用例,运行所测程序,使程序中每个判断的取真分支和取假分支至少执行一次。
条件覆盖
设计足够的测试用例,运行所测程序,使程序中每个判断的每个条件的每个可能取值至少执行一次。
判定-条件覆盖
使程序中每个判断的每个条件的每个可能取值至少执行一次,并且每个可能的判断结果也至少执行一次。
条件组合测试
设计足够的测试用例,运行所测程序,使程序中每个判断的所有条件取值组合至少执行一次
路径测试
设计足够的测试用例,运行所测程序,要覆盖程序中所有可能的路径。
单元测试实战
单元测试工具:
JUnit JMock Ant
实例代码:
package cn.net.inch.unittest; public interface IBankService { void setInterestStrategy(IInterestStrategy interestStrategy); void checkout(BankAccount account); }
package cn.net.inch.unittest; /** * @author yellowcat * 银行相关业务 */ public class BankService implements IBankService { private IInterestStrategy interestStrategy; public void setInterestStrategy(IInterestStrategy interestStrategy) { this.interestStrategy = interestStrategy; } /** * 对银行账号进行结算 * 现有的余额等于本金加上利息所得 * @param account 银行账号 * @return void * @throws EmptyAccountException */ public void checkout(BankAccount account) { if (account == null || account.getId() == 0) { throw new EmptyAccountException("银行账号不能为空"); } double amount = account.getAmount(); amount += interestStrategy.calculateInterest(account.getAmount(), account.getRate()); account.setAmount(amount); } }
package cn.net.inch.unittest; /** * @author yellowcat * 银行账号 */ public class BankAccount { private int id; // 账号ID private String name; //账号名称 private double amount; //账号余额 private double rate; //存款利率 public BankAccount(int id, String name, double amount, double rate) { super(); this.id = id; this.name = name; this.amount = amount; this.rate = rate; } public BankAccount() { } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } public double getRate() { return rate; } public void setRate(double rate) { this.rate = rate; } }
package cn.net.inch.unittest; /** * @author yellowcat * */ public class EmptyAccountException extends RuntimeException { private static final long serialVersionUID = 6403530909283386537L; public EmptyAccountException(String message) { super(message); } }
package cn.net.inch.unittest; /** * * @author yellowcat * * 利息计算策略接口 * 要求有两种利息计算策略: * 包含利息税的和不包含的 */ public interface IInterestStrategy { double calculateInterest(double amount, double rate); }
package cn.net.inch.unittest; /** * @author yellowcat * */ public class InterestStrategyWithTax implements IInterestStrategy { private static final double INTEREST_TAX = 0.2; /* (non-Javadoc) * @see cn.net.inch.unittest.IInterestStrategy#calculateInterest(double, double) */ public double calculateInterest(double amount, double rate) { return amount * rate * (1 - INTEREST_TAX); } }
package cn.net.inch.unittest; /** * @author yellowcat * */ public class InterestStrategyWithoutTax implements IInterestStrategy { /* (non-Javadoc) * @see cn.net.inch.unittest.IInterestStrategy#calculateInterest(double, double) */ @Override public double calculateInterest(double amount, double rate) { return amount * rate; } }
测试代码:
package cn.net.inch.unittest; import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; /** * @author yellowcat * */ public class BankServiceTest { private BankService bankService; /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception { bankService = new BankService(); } /** * @throws java.lang.Exception */ @After public void tearDown() throws Exception { // nothing to do } /** * 测试包含利息税的结算 */ @Test public void testCheckoutWithInterestTax() { IInterestStrategy interestStrategy = new InterestStrategyWithTax(); bankService.setInterestStrategy(interestStrategy); BankAccount accontToTest = new BankAccount(1, "harry", 10000D, 0.0225D); bankService.checkout(accontToTest); assertEquals(10180D, accontToTest.getAmount()); } /** * 测试不包含利息税的结算 */ @Test public void testCheckoutWithoutInterestTax() { IInterestStrategy interestStrategy = new InterestStrategyWithoutTax(); bankService.setInterestStrategy(interestStrategy); BankAccount accontToTest = new BankAccount(1, "harry", 10000D, 0.0225D); bankService.checkout(accontToTest); assertEquals(10225D, accontToTest.getAmount()); } /** * 测试账号为空异常 */ @Test public void testCheckoutWithEmptyAccount() { BankAccount accontToTest = new BankAccount(); try { bankService.checkout(accontToTest); fail("没有抛出账号为空异常!"); } catch (EmptyAccountException eae) { // what I except to } } }
package cn.net.inch.unittest; import static org.junit.Assert.*; import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.After; import org.junit.Before; import org.junit.Test; /** * @author yellowcat * */ public class BankServiceJMockTest { private BankService bankService; // mock factory private Mockery context = new Mockery(); /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception { bankService = new BankService(); } /** * @throws java.lang.Exception */ @After public void tearDown() throws Exception { // nothing to do } /** * Test method for {@link cn.net.inch.unittest.BankService#checkout(cn.net.inch.unittest.BankAccount)}. */ @Test public void testCheckout() { BankAccount accontToTest = new BankAccount(1, "harry", 10000D, 0.0225D); // set up final IInterestStrategy interestStrategy = context.mock(IInterestStrategy.class); // expectations context.checking(new Expectations() {{ allowing(interestStrategy).calculateInterest(10000D, 0.0225D); will(returnValue(225D)); }}); // execute bankService.setInterestStrategy(interestStrategy); bankService.checkout(accontToTest); // verify context.assertIsSatisfied(); assertEquals(10225D, accontToTest.getAmount()); } }
Ant脚本:
<project name="unittest" default="junit-report" basedir="."> <property name="bin" value="bin" /> <property name="src" value="src" /> <property name="lib" value="lib" /> <property name="test.src" value="test" /> <property name="test.report" value="report" /> <target name="test-init" description="test report folder init"> <mkdir dir="${test.report}" /> </target> <path id="lib.classpath"> <fileset dir="${lib}"> <include name="*.jar" /> </fileset> </path> <target name="compile"> <javac classpathref="lib.classpath" srcdir="${src}" destdir="${bin}" /> <echo>compilation complete!</echo> </target> <target name="test-compile" depends="test-init" description="compile test cases"> <javac classpathref="lib.classpath" srcdir="${test.src}" destdir="${bin}" /> <echo>test compilation complete!</echo> </target> <target name="compile-all" depends="compile, test-compile"> </target> <target name="junit-report" depends="compile-all" description="auto test all test case and output report file"> <junit printsummary="on" fork="true" showoutput="true"> <classpath> <fileset dir="${lib}" includes="*.jar" /> <pathelement path="${bin}" /> </classpath> <formatter type="xml" /> <batchtest todir="${test.report}"> <fileset dir="${bin}"> <include name="**/*Test.*" /> </fileset> </batchtest> </junit> <junitreport todir="${test.report}"> <fileset dir="${test.report}"> <include name="TEST-*.xml" /> </fileset> <report format="frames" todir="${test.report}" /> </junitreport> </target> </project>
测试报告: