★ P02项目诊断报警组件
诊断报警组件的主要功能有:
- 接收、记录硬件设备上报的报警信息。
- 从预先设定的错误码对照表中找到对应的声光报警和蜂鸣器报警策略,结合当前的报警情况对设备下发报警指示。
- 将报警消息发送到消息队列,由其它组件发送给前端。
- 从消息队列获取用户通过前端进行的报警处理操作,更新故障报警表对应记录的处理信息,并根据预定规则调整当前的声光报警、蜂鸣器报警策略。
- 定期往Redis缓存写入数据,并将该数据的有效期设为写入周期的3倍。
- 定期从Redis缓存中获取系统监视组件写入的标志信息,如果获取失败则表示系统监视组件出现故障,产生报警记录并对设备下发报警指示。
- 系统监视组件产生的后端组件故障报警信息的记录和处理。
- 从Redis缓存获取当前系统配置参数中的各优先级报警循环时间,并在配置参数有改动时及时更新。
- 用户通过前端页面对历史报警信息进行查询。
职责描述:
1、学习项目。
2、单元测试
学习别人的如何操作日志记录注解
java
package com.wg.common.annotation;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
String business() default "";
}
java
package com.wg.common.aspectj;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.wg.common.annotation.Log;
import com.wg.common.constant.Constants;
import com.wg.common.entity.OperationLog;
import com.wg.common.entity.User;
import com.wg.common.service.IOperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 日志aop信息
**/
@Slf4j
@Aspect
@Component
public class LogAspect {
@Autowired
private IOperationLogService operationLogService;
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(Log controllerLog, Object jsonResult) {
handleLog(controllerLog, null, jsonResult);
}
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(Log controllerLog, Exception e)
{
handleLog(controllerLog, e, null);
}
protected void handleLog(Log controllerLog, final Exception e, Object jsonResult)
{
try
{
User user = (User) StpUtil.getSession().get("user");
OperationLog operationLog = new OperationLog();
operationLog.setStatus(Constants.SUCCESS);
if (ObjectUtil.isNotNull(user))
{
operationLog.setUserName(user.getUserName());
operationLog.setNickName(user.getNickName());
}
if (e != null)
{
operationLog.setStatus(Constants.FAIL);
operationLog.setErrorMsg(StrUtil.sub(e.getMessage(), 0, 2000));
}
operationLog.setTitle(controllerLog.title());
operationLog.setOperationTime(new Date());
operationLogService.save(operationLog);
}
catch (Exception exp)
{
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
}
学习单元测试
首先实际完成的时候偷懒的方式就是用SquareTest生成。
Controller 测试
Spring 提供了 MockMVC 用于支持 RESTful 风格的 Spring MVC 测试,使用 MockMvcBuilder 来构造MockMvc 实例。MockMvc 有两个实现:
StandaloneMockMvcBuilder:指定 WebApplicationContext,它将会从该上下文获取相应的控制器并得到相应的 MockMvc
java
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
DefaultMockMvcBuilder:通过参数指定一组控制器,这样就不需要从上下文获取了
java
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
}
}
下面是一个简单的用例,对 UserController 的 /user/{id} 接口进行测试。
java
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("/{id}")
public User get(@PathVariable("id") String id) {
return new User(1, "lst");
}
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String name;
}
}
java
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void getUser() {
mockMvc.perform(get("/user/1")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"name\":\"lst\"")));
}
}
方法描述
perform:执行一个 RequestBuilder 请求,返回一个 ResultActions 实例对象,可对请求结果进行期望与其它操作
get:声明发送一个 get 请求的方法,更多的请求类型可查阅→MockMvcRequestBuilders 文档
andExpect:添加 ResultMatcher 验证规则,验证请求结果是否正确,验证规则可查阅→MockMvcResultMatchers 文档
andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台,更多处理器可查阅→MockMvcResultHandlers 文档
andReturn:返回执行请求的结果,该结果是一个恩 MvcResult 实例对象→MvcResult 文档
Mock 数据
在单元测试中,Service 层的调用往往涉及到对数据库、中间件等外部依赖。
如果不需要对静态方法,私有方法等特殊进行验证测试,则仅仅使用 Spring boot 自带的 Mockito 即可完成相关的测试数据 Mock。若需要则可以使用 PowerMock,简单实用,结合 Spring 可以使用注解注入。
@MockBean
SpringBoot 在执行单元测试时,会将该注解的 Bean 替换掉 IOC 容器中原生 Bean。
例如下面代码中, ProjectService 中通过 ProjectMapper 的 selectById 方法进行数据库查询操作:
java
@Service
public class ProjectService {
@Autowired
private ProjectMapper mapper;
public ProjectDO detail(String id) {
return mapper.selectById(id);
}
}
此时我们可以对 Mock 一个 ProjectMapper 对象替换掉 IOC 容器中原生的 Bean,来模拟数据库查询操作,如:
复制代码
java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProjectServiceTest {
@MockBean
private ProjectMapper mapper;
@Autowired
private ProjectService service;
@Test
public void detail() {
ProjectDemoDO model = new ProjectDemoDO();
model.setId("1");
model.setName("dubbo-demo");
Mockito.when(mapper.selectById("1")).thenReturn(model);
ProjectDemoDO entity = service.detail("1");
assertThat(entity.getName(), containsString("dubbo-demo"));
}
}
Mockito 常用方法
mock() 对象
List list = mock(List.class);
verify() 验证互动行为
java
@Test
public void mockTest() {
List list = mock(List.class);
list.add(1);
// 验证 add(1) 互动行为是否发生
Mockito.verify(list).add(1);
}
when() 模拟期望结果
java
@Test
public void mockTest() {
List list = mock(List.class);
when(mock.get(0)).thenReturn("hello");
assertThat(mock.get(0),is("hello"));
}
doThrow() 模拟抛出异常
java
@Test(expected = RuntimeException.class)
public void mockTest(){
List list = mock(List.class);
doThrow(new RuntimeException()).when(list).add(1);
list.add(1);
}
@Mock 注解
在上面的测试中我们在每个测试方法里都 mock 了一个 List 对象,为了避免重复的 mock,使测试类更具有可读性,我们可以使用下面的注解方式来快速模拟对象:
java
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
@Mock
private List list;
public MockitoTest(){
// 初始化 @Mock 注解
MockitoAnnotations.initMocks(this);
}
@Test
public void shorthand(){
list.add(1);
verify(list).add(1);
}
}
when() 参数匹配
java
@Test
public void mockTest(){
Comparable comparable = mock(Comparable.class);
//预设根据不同的参数返回不同的结果
when(comparable.compareTo("Test")).thenReturn(1);
when(comparable.compareTo("Omg")).thenReturn(2);
assertThat(comparable.compareTo("Test"),is(1));
assertThat(comparable.compareTo("Omg"),is(2));
//对于没有预设的情况会返回默认值
assertThat(list.get(1),is(999));
assertThat(comparable.compareTo("Not stub"),is(0));
}
spy() 监控真实对象
Mock 不是真实的对象,它只是创建了一个虚拟对象,并可以设置对象行为。而 Spy是一个真实的对象,但它可以设置对象行为。
java
@Test(expected = IndexOutOfBoundsException.class)
public void mockTest(){
List list = new LinkedList();
List spy = spy(list);
//下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
when(spy.get(0)).thenReturn(3);
//使用doReturn-when可以避免when-thenReturn调用真实对象api
doReturn(999).when(spy).get(999);
//预设size()期望值
when(spy.size()).thenReturn(100);
//调用真实对象的api
spy.add(1);
spy.add(2);
assertThat(spy.size(),is(100));
assertThat(spy.size(),is(1));
assertThat(spy.size(),is(2));
verify(spy).add(1);
verify(spy).add(2);
assertThat(spy.get(999),is(999));
}
reset() 重置 mock
java
@Test
public void reset_mock(){
List list = mock(List.class);
when(list.size()).thenReturn(10);
list.add(1);
assertThat(list.size(),is(10));
//重置mock,清除所有的互动和预设
reset(list);
assertThat(list.size(),is(0));
}
times() 验证调用次数
java
@Test
public void verifying_number_of_invocations(){
List list = mock(List.class);
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
//验证是否被调用一次,等效于下面的times(1)
verify(list).add(1);
verify(list,times(1)).add(1);
//验证是否被调用2次
verify(list,times(2)).add(2);
//验证是否被调用3次
verify(list,times(3)).add(3);
//验证是否从未被调用过
verify(list,never()).add(4);
//验证至少调用一次
verify(list,atLeastOnce()).add(1);
//验证至少调用2次
verify(list,atLeast(2)).add(2);
//验证至多调用3次
verify(list,atMost(3)).add(3);
}
inOrder() 验证执行顺序
java
@Test
public void verification_in_order(){
List list = mock(List.class);
List list2 = mock(List.class);
list.add(1);
list2.add("hello");
list.add(2);
list2.add("world");
//将需要排序的mock对象放入InOrder
InOrder inOrder = inOrder(list,list2);
//下面的代码不能颠倒顺序,验证执行顺序
inOrder.verify(list).add(1);
inOrder.verify(list2).add("hello");
inOrder.verify(list).add(2);
inOrder.verify(list2).add("world");
}