1、A\B测试
1、是什么
一个好的产品都是迭代出来的,而我们很可能不清楚这次的迭代最终是好是坏(至少我们是觉得迭代对用户是好的,是有帮助的,对公司的转化也是好的),但是我们的用户未必就买账。
ABTest最主要做的就是一个分流:
- 将10%流量分给用户群体A
- 将10%流量分给用户群体B
我们需要保证的是:一个用户再次请求进来,用户看到的结果是一样的
一般可以这样做:
- 对 用 户 ID( 设 备 ID/CookieId/userId/openId) 取hash值,每 次 Hash 的 结 果 都 是 相 同的。
- 直接取用户ID的某一位
2、性能测试-执行时间
2.1 简单统计方式
start/end =
System.currentTimeMillis()
System.nanoTime()
new Date()
2.2 框架统计
- commons-langs3
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Thread.sleep(1000);
stopWatch.stop();
- guava
Stopwatch stopwatch = Stopwatch.createStarted();
stopwatch.stop();
stopwatch.elapsed(TimeUnit.MILLISECONDS)
3、单元测试
3.1 接口层测试
对于Java开发的开发,主要是Springboot的接口进行mock测试。
下面是单侧注解说明:
1、@RunWith:
该注解标签是Junit提供的,用来说明此测试类的运行者,这里用了SpringRunner,
它实际上继承了 SpringJUnit4ClassRunner类,而 SpringJUnit4ClassRunner
这个类是一个针对Junit 运行环境的自定义扩展,用来标准化在Springboot环境下Junit4.x的测试用例
@RunWith就是一个运行器
@RunWith(JUnit4.class)就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境
@RunWith(Suite.class)的话就是一套测试集合
@RunWith(Parameterized.class)
2、@SpringBootTest:
为springApplication创建上下文并支持SpringBoot特性
@SpringBootTest的webEnvironment属性定义运行环境:
Mock(默认):
此值为默认值,该类型提供一个mock环境,可以和@AutoConfigureMockMvc或@AutoConfigureWebTestClient搭配使用,开启Mock相关的功能。注意此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web服务端口。
RANDOM_PORT:
加载WebServerApplicationContext 并提供真实的web环境,嵌入式服务器,监听端口是随机的
DEFINED_PORT:
加载WebServerApplicationContext并提供真实的Web环境,嵌入式服务器启动并监听定义的端口(来自 application.properties或默认端口 8080)
NONE: 启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。
写单元测试,难免需要操作数据库。有时候单元测试的数据库跟开发时候的数据库是同一个,为了不影响数据库的数据,需要在单测完成之后,将操作回滚。这在springboot中也是很容易解决的事情,只需要将单测类继承AbstractTransactionalJUnit4SpringContextTests即可。
3.1.1 模拟环境进行测试(不启动服务器)
- @AutoConfigureMockMvc:
该注解表示启动测试的时候自动注入 MockMvc,而这个MockMvc有以下几个基本的方法: - perform : 执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理。
- andExpect: 添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确
- andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
- andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void userMapping() throws Exception {
String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
mockMvc.perform(request(HttpMethod.POST, "/user")
.contentType("application/json").content(content))
.andExpect(status().isOk())
.andExpect(content().string("ok"));
}
@Test
public void TestUpgradeApp() throws Exception{
RequestBuilder request = null;
request = get("/appProducer/upgradeApp")
.param("appId", "1001");
mockMvc.perform(request)
.andExpect(status().isOk())//返回HTTP状态为200
.andDo(print());//打印结果
}
}
3.1.2 真实Web环境进行测试(启动一个Spring应用程序上下文)
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest3 {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void userMapping() throws Exception {
User user = new User();
user.setUsername("pj_pj");
user.setPassword("123456");
ResponseEntity<String> responseEntity =
testRestTemplate.postForEntity("/user", user, String.class);
System.out.println("Result: "+responseEntity.getBody());
System.out.println("状态码: "+responseEntity.getStatusCodeValue());
}
}
3.1.3 仅Web层启动
将启动完整的Spring应用程序上下文,但没有服务器,测试范围缩小到仅Web层
@WebMvcTest(GreetingController.class)
public class WebMockTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private GreetingService service;
@Test
public void greetingShouldReturnMessageFromService() throws Exception {
when(service.greet()).thenReturn("Hello, Mock");
this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
.andExpect(content().string(containsString("Hello, Mock")));
}
}
3.2 业务层测试
3.2.1 Junit
- 常用注解
@BeforeClass
@Before
@Test
@Test(expected=xxxException.class) 断言该方法会抛出异常
@Test(timeout=1000) 执行时间超过设置的值该案例会失败
@RunWith(JUnit4.class) 默认运行器
@Suite.SuiteClasses({ CalculatorTest.class,SquareTest.class})
JUnit测试套件:
@RunWith(Suite.class)
测试集运行器配合使用测试集功能
如果您有多个测试类,那么可以将它们组合成一个测试套件。
运行测试套件将以指定顺序执行该套件中的所有测试类。
一个测试套件也可以包含其他测试套件。
- 测试实例
@RunWith(Parameterized.class) 参数化运行器
public class PrimeFactorTest {
private PrimeFactor primeFactor;
private int input;
private List<Integer> expected;
//构造函数
public PrimeFactorTest(int input, List<Integer> expected) {
this.input = input;
this.expected = expected;
}
@Parameterized.Parameters
public static Collection init() {
return Arrays.asList(new Object[][]{
{18, Arrays.asList(2, 3, 3)}
});
}
@Test
public void testFactor_when_input_18_then_must_return_2_3_3() {
Assert.assertEquals(expected, primeFactor.factor(input));
}
}
使用JUnit Rules:
您可以为测试类中的每个测试添加行为。您可以使用@Rule注解来标注TestRule类型的字段。
3.2.2 AssertThat结果验证
常用的断言方法如下:
assertEquals(a, b) 测试a是否等于b(a和b是原始类型数值(primitive value)或者必须为实现比较而具有equal方法)
assertFalse(a) 测试a是否为false(假),a是一个Boolean数值。
assertTrue(a) 测试a是否为true(真),a是一个Boolean数值
assertNotNull(a) 测试a是否非空,a是一个对象或者null。
assertNull(a) 测试a是否为null,a是一个对象或者null。
assertNotSame(a, b) 测试a和b是否没有都引用同一个对象。
assertSame(a, b) 测试a和b是否都引用同一个对象。
fail(string) Fail让测试失败,并给出指定信息。
assertThat(expected, Matcher) 通过Matcher断言
assertThat替代assertion
Assertions
ReflectionTestUtils
补充: 流式断言器AssertJ介绍
3.2.3 Mockit
//快速创建Mock对象
@Mock
private List mockList;
// 验证行为
@Test
public void verify_behaviour(){
// 模拟List 的一个对象
List mock = mock(List.class);
mock.add(1);
mock.clear();
verify(mock).add(2);
verify(mock).clear();
}
@Test
public void verify_behaviour2(){
mockList.add(1);
// 均验证add方法是否被调用1
verify(mockList).add(1);
}
// 模拟期望结果
@Test
public void when_thenReturn() {
Iterator iterator = mock(Iterator.class);
// 模拟方法调用的返回值
when(iterator.next()).thenReturn("hello");
String result = iterator.next() + " " + iterator.next();
assertEquals("hello world",result);
}
// 模拟方法体抛出异常
@Test(expected = RuntimeException.class)
public void doThrow_when(){
// 模拟获取第二个元素时,抛出RuntimeException
when(mockedList.get(1)).thenThrow(new RuntimeException());
List list = mock(List.class);
如果一个函数没有返回值类型,那么可以使用此方法模拟异常抛出
doThrow(new RuntimeException()).when(list).add(1);
list.add(1);
}
// 参数匹配
@Test
public void with_arguments(){
Comparable comparable = mock(Comparable.class);
when(comparable.compareTo("Test")).thenReturn(1);
when(comparable.compareTo("Omg")).thenReturn(2);
assertEquals(1, comparable.compareTo("Test"));
assertEquals(2, comparable.compareTo("Omg"));
assertEquals(0, comparable.compareTo("Not stub"));
}
// 包装实际对象
@Test
public void spy_on_real_objects(){
List list = new LinkedList();
List spy = Mockito.spy(list);
// 对监控对象的行为回调判断
// 每次调用都委托给实际对象
// when(spy.get(0)).thenReturn(100);
doReturn(100).when(spy).get(0);
Assert.assertEquals(100,spy.get(0));
}
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using the spy calls *real* methods
spy.add("one");
spy.add("two");
//prints "one" - the first element of a list
System.out.println(spy.get(0));
//size() method was stubbed - 100 is printed
System.out.println(spy.size());
//optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
Foo mock = mock(Foo.class);
//Be sure the real implementation is 'safe'.
//If real implementation throws exceptions or
depends on specific state of the object then you're in trouble.
when(mock.someMethod()).thenCallRealMethod();
3.2.4 JMockit
@Mocked
Calendar cal;
@Test
public void testRecordOutside() {
new Expectations() {
{
cal.get(Calendar.YEAR);
result = 2016;
cal.get(Calendar.HOUR_OF_DAY);
result = 7;
}
};
Assert.assertTrue(cal.get(Calendar.YEAR) == 2016);
Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == 0);
new Verifications() {
{
cal.get(Calendar.YEAR);
times = 1;
}
};
}
4、测试流程
- 流程
1.数据准备
2.执行测试
3.验证结果
4.数据清理
mysql: 使用@Transactional注解回滚数据
redis: 测试方法的最后执行清理
- 分层测试
dao:h2
service: mockit+juint
controller: mvcTest
mock:
controller 层主要测试参数校验逻辑,测试的时候并不需要真正的调用 service 层服务,
可以通过 mock 框架将 service 方法 mock 掉
service 层主要测试业务逻辑是否正确,当 service 层发生调用外部服务的时候,
需要 mock 掉外部服务的调用代码,避免单元测试的时候调用外部服务,导致外部服务异常。
同时单元测试不应该依赖外部服务