一、单元测试
首先对于单元测试,我们需要先了解下junit和testng(该部分转至https://www.mkyong.com/unittest/junit-4-vs-testng-comparison/)
JUnit 4和TestNG都是Java中非常流行的单元测试框架。两个框架在功能上看起来非常相似,TestNG是一套根据JUnit和NUnit思想而构建的利用注释来强化测试功能的一个测试框架,既可以用来做单元测试,也可以用来做集成测试。
TestNG是一个开源的自动化测试框架,NG表示Next Generation,TestNG即表示下一代测试技术。
下面表中概括了JUnit 4和TestNG之间的功能比较。如下图所示
1. 注解支持
注释/注解支持在JUnit 4和TestNG中是非常类似的。
JUnit4和TestNG之间的主要注释差异是:
在JUnit 4中,我们必须声明“@BeforeClass”和“@AfterClass”方法作为静态方法。 TestNG在方法声明中更灵活,它没有这个约束。TestNg有3个额外的setUp / tearDown级别:suite和group(@BeforeSuite / @AfterSuite,@BeforeTest / AfterTest,@BeforeGroup / AfterGroup)。
JUnit 4
@BeforeClass
public static void oneTimeSetUp() {
// one-time initialization code
System.out.println("@BeforeClass - oneTimeSetUp");
}
TestNG
@BeforeClass
public void oneTimeSetUp() {
// one-time initialization code
System.out.println("@BeforeClass - oneTimeSetUp");
}
2. 异常测试
“异常测试”是指从单元测试中抛出的异常,此功能在JUnit 4和TestNG中都可实现。
JUnit 4
@Test(expected = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}
TestNG
@Test(expectedExceptions = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}
3. 忽略测试
“忽略”表示是否应该忽略单元测试,该功能在JUnit 4和TestNG中均可实现。
JUnit 4
@Ignore("Not Ready to Run")
@Test
public void divisionWithException() {
System.out.println("Method is not ready yet");
}
TestNG
@Test(enabled=false)
public void divisionWithException() {
System.out.println("Method is not ready yet");
}
4. 时间测试
“时间测试”表示如果单元测试所花费的时间超过指定的毫秒数,则测试将会终止,并将其标记为失败,此功能在JUnit 4和TestNG中均可实现。
JUnit 4
@Test(timeout = 1000)
public void infinity() {
while (true);
}
TestNG
@Test(timeOut = 1000)
public void infinity() {
while (true);
}
5. 套件测试
“套件测试”是指捆绑几个单元测试并一起运行。 此功能在JUnit 4和TestNG中都可实现。 然而,两者都使用非常不同的方法来实现它。
JUnit 4
“@RunWith”和“@Suite”用于运行套件测试。下面的类代码表示在JunitTest5执行之后,单元测试“JunitTest1”和“JunitTest2”一起运行。 所有的声明都是在类内定义的。
@RunWith(Suite.class)
@Suite.SuiteClasses({
JunitTest1.class,
JunitTest2.class
})
public class JunitTest5 {
}
TestNG
XML文件用于运行套件测试。以下XML文件表示单元测试“TestNGTest1”和“TestNGTest2”将一起运行。
<suite name="My test suite">
<test name="testing">
<classes>
<class name="com.fsecure.demo.testng.TestNGTest1" />
<class name="com.fsecure.demo.testng.TestNGTest2" />
</classes>
</test>
</suite>
XML
TestNG可以做捆绑类测试,也可以捆绑方法测试。 凭借TestNG独特的“分组”概念,每种方法都可以与一个组合相结合,可以根据功能对测试进行分类(分组)。 例如,
下面是一个有四个方法的类,三个组(method1,method2和method3)
@Test(groups="method1")
public void testingMethod1() {
System.out.println("Method - testingMethod1()");
}
@Test(groups="method2")
public void testingMethod2() {
System.out.println("Method - testingMethod2()");
}
@Test(groups="method1")
public void testingMethod1_1() {
System.out.println("Method - testingMethod1_1()");
}
@Test(groups="method4")
public void testingMethod4() {
System.out.println("Method - testingMethod4()");
}
使用以下XML文件,可以仅使用组“method1”执行单元测试。
<suite name="My test suite">
<test name="testing">
<groups>
<run>
<include name="method1"/>
</run>
</groups>
<classes>
<class name="com.fsecure.demo.testng.TestNGTest5_2_0" />
</classes>
</test>
</suite>
通过“分组”测试概念,集成测试的可能性是无限制的。 例如,我们只能从所有单元测试类中测试“DatabaseFuntion”分组。
6. 参数化测试
“参数化测试”是指单位测试参数值的变化。 此功能在JUnit 4和TestNG中都实现。 然而,两者都使用非常不同的方法来实现它。
JUnit 4
“@RunWith”和“@Parameter”用于提供单元测试的参数值,@Parameters必须返回List [],参数将作为参数传入类构造函数。
@RunWith(value = Parameterized.class)
public class JunitTest6 {
private int number;
public JunitTest6(int number) {
this.number = number;
}
@Parameters
public static Collection<Object[]> data() {
Object[][] data = new Object[][] { { 1 }, { 2 }, { 3 }, { 4 } };
return Arrays.asList(data);
}
@Test
public void pushTest() {
System.out.println("Parameterized Number is : " + number);
}
}
这里有很多限制,我们必须遵循“JUnit”的方式来声明参数,并且必须将参数传递给构造函数才能初始化类成员作为测试的参数值。参数类的返回类型为“List []”,数据已被限制为String或用于测试的原始类型值。
TestNG
XML文件或“@DataProvider”用于提供不同参数进行测试。
用于参数化测试的XML文件 –
只有“@Parameters”在需要参数测试的方法中声明,参数化数据将在TestNG的XML配置文件中提供。 通过这样做,我们可以使用不同数据集的单个测试用例,甚至获得不同的结果。 另外,即使是最终用户,QA还是QE都可以在XML文件中提供自己的数据进行测试。
public class TestNGTest6_1_0 {
@Test
@Parameters(value="number")
public void parameterIntTest(int number) {
System.out.println("Parameterized Number is : " + number);
}
}
XML文件的内容如下 –
<suite name="My test suite">
<test name="testing">
<parameter name="number" value="2"/>
<classes>
<class name="com.fsecure.demo.testng.TestNGTest6_0" />
</classes>
</test>
</suite>
@DataProvider用于参数化测试
将数据值拉入XML文件可能非常方便,但测试偶尔会需要复杂的类型,这些类型不能被表示为一个字符串或一个原始类型值。 TestNG使用@DataProvider注解来处理这种情况,这有助于将复杂参数类型映射到测试方法。
@DataProvider for Vector,String或Integer作为参数,参考如下代码 –
@Test(dataProvider = "Data-Provider-Function")
public void parameterIntTest(Class clzz, String[] number) {
System.out.println("Parameterized Number is : " + number[0]);
System.out.println("Parameterized Number is : " + number[1]);
}
//This function will provide the patameter data
@DataProvider(name = "Data-Provider-Function")
public Object[][] parameterIntTestProvider() {
return new Object[][]{
{Vector.class, new String[] {"java.util.AbstractList",
"java.util.AbstractCollection"}},
{String.class, new String[] {"1", "2"}},
{Integer.class, new String[] {"1", "2"}}
};
}
@DataProvider作为对象的参数
“TestNGTest6_3_0”是一个简单的对象,只需使用get/set方法进行演示。
@Test(dataProvider = "Data-Provider-Function")
public void parameterIntTest(TestNGTest6_3_0 clzz) {
System.out.println("Parameterized Number is : " + clzz.getMsg());
System.out.println("Parameterized Number is : " + clzz.getNumber());
}
//This function will provide the patameter data
@DataProvider(name = "Data-Provider-Function")
public Object[][] parameterIntTestProvider() {
TestNGTest6_3_0 obj = new TestNGTest6_3_0();
obj.setMsg("Hello");
obj.setNumber(123);
return new Object[][]{
{obj}
};
}
TestNG的参数化测试非常用户友好和灵活(在XML文件或类内)。 它可以支持许多复杂的数据类型作为参数值,可能性是无限的。 如上例所示,我们甚至可以传入我们自己的对象(TestNGTest6_3_0)进行参数化测试
7.依赖性测试
“参数化测试”表示方法是依赖性测试,它将在所需方法之前执行。 如果依赖方法失败,则所有后续测试将会被跳过,不会被标记为失败。
JUnit 4
JUnit框架着重于测试隔离; 目前它不支持此功能。
TestNG
它使用“dependOnMethods”来实现依赖测试如下 –
@Test
public void method1() {
System.out.println("This is method 1");
}
@Test(dependsOnMethods={"method1"})
public void method2() {
System.out.println("This is method 2");
}
“method2()”只有在“method1()”运行成功的情况下才会执行,否则“method2()”将跳过测试。
在考虑所有功能比较之后,建议使用TestNG作为Java项目的核心单元测试框架,因为TestNG在参数化测试,依赖测试和套件测试(分组概念)方面更加突出。 TestNG用于高级测试和复杂集成测试。 它的灵活性对于大型测试套件尤其有用。 此外,TestNG还涵盖了整个核心的JUnit4功能。这样说来,好像也没有理由使用JUnit了。
二、mock方法
单元测试中,我们往往想去独立地去测一个类中的某个方法,但是这个类可能会去调用一些其它类的方法,这也就导致了以下问题:外部服务可能无法在单元测试的环境中正常工作,因为它们可能需要访问数据库或者使用一些其它的外部系统。外部类的一些行为可能会影响到我们对本类的测试,那也就失去了我们进行单测的意义。为了解决这种问题,Mockito和PowerMock诞生了。这两种工具都可以用一个虚拟的对象来替换那些外部的、不容易构造的对象,便于我们进行单元测试。两者的不同点在于Mockito对于大多数的标准单测case都很适用,而PowerMock可以去解决一些更难的问题(比如静态方法、私有方法等)。
简单来说,mock就是
- mock对象就是在调试期间用来作为真实对象的替代品,俗称假类
- mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试
2.1 Spring中利用PowerMockito
一般使用Spring,都会用到依赖注入(DI)。
@Service
public class SampleService {
@Autowired
private SampleDependency dependency;
public String foo() {
return dependency.getExternalValue("bar");
}
}
如果测试中需要对Sping注入的对象进行注入,该怎么做呢?
一. 当使用junit来测试Spring的代码时,为了减少依赖,需要给对象的依赖,设置一个mock对象,但是由于Spring可以使用@Autoware类似的注解方式,对私有的成员进行赋值,此时无法直接对私有的依赖设置mock对象。可以通过引入ReflectionTestUtils,解决依赖注入的问题。
@Mock
HumanDetectDao humanDetectDao;
@BeforeClass
public void init(){
humanDetectDao = PowerMockito.mock(HumanDetectDao.class);
ReflectionTestUtils.setField(targetObject, "humanDetectDao", humanDetectDao, HumanDetectDao.class);
}
选择一 绕过限制
当使用junit来测试Spring的代码时,为了减少依赖,需要给对象的依赖,设置一个mock对象,但是由于Spring可以使用@Autoware类似的注解方式,对私有的成员进行赋值,此时无法直接对私有的依赖设置mock对象。可以通过引入ReflectionTestUtils,解决依赖注入的问题。
SampleDependency dependency = mock(SampleDependency.class);
SampleService service = new SampleService();
ReflectionTestUtils.setField(service, "dependency", dependency);
@Mock
HumanDetectDao humanDetectDao;
@BeforeClass
public void init(){
humanDetectDao = PowerMockito.mock(HumanDetectDao.class);
ReflectionTestUtils.setField(targetObject, "humanDetectDao", humanDetectDao, HumanDetectDao.class);
}
选择二 使用Mockito InjectMocks
这里推荐使用mockito 的InjectMocks注解。测试可以写成
@Rule public MockitoRule rule = MockitoJUnit.rule();
@Mock SampleDependency dependency;
@InjectMocks SampleService sampleService;
对应于实现代码中的每个@Autowired
字段,测试中可以用一个@Mock
声明mock对象,并用@InjectMocks
标示需要注入的对象。
这里的
MockitoRule
的作用是初始化mock对象和进行注入的。有三种方式做这件事。
- 测试
@RunWith(MockitoJUnitRunner.class)
- 使用rule
@Rule public MockitoRule rule = MockitoJUnit.rule();
- 调用
MockitoAnnotations.initMocks(this)
,一般在setup方法中调用
InjectMocks可以和Sping的依赖注入结合使用。比如:
@RunWith(SpringRunner.class)
@DirtiesContext
@SpringBootTest
public class ServiceWithMockTest {
@Rule public MockitoRule rule = MockitoJUnit.rule();
@Mock DependencyA dependencyA;
@Autowired @InjectMocks SampleService sampleService;
@Test
public void testDependency() {
when(dependencyA.getExternalValue(anyString())).thenReturn("mock val: A");
assertEquals("mock val: A", sampleService.foo());
}
}
假定Service注入了2个依赖dependencyA, dependencyB。上面测试使用Spring注入了B,把A替换为mock对象。
需要注意的是,Spring test默认会重用bean。如果另有一个测试也使用注入的SampleService并在这个测试之后运行,那么拿到service中的dependencyA仍然是mock对象。一般这是不期望的。所以需要用@DirtiesContext
修饰上面的测试避免这个问题。
选择三 Springboot MockBean
如果使用的是Springboot,测试可以用MockBean更简单的写出等价的测试。
@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceWithMockBeanTest {
@MockBean SampleDependencyA dependencyA;
@Autowired SampleService sampleService;
@Test
public void testDependency() {
when(dependencyA.getExternalValue(anyString())).thenReturn("mock val: A");
assertEquals("mock val: A", sampleService.foo());
}
}