如何在 Spring MVC 应用程序中测试方面
How to test Aspect in Spring MVC application
我有一个 Spring MVC 应用程序,我在其中使用方面来捕获所有控制器方法中的异常
@Component
@Aspect
public class ControllerExceptionAspect {
private Logger logger;
public ControllerExceptionAspect() {
logger = Logger.getLogger(ControllerExceptionAspect.class);
}
public ControllerExceptionAspect(Logger logger) {
this.logger = logger;
}
// Catching all exceptions from all methods in all controllers classes
@AfterThrowing(pointcut = "execution(* com.my.package..controller..*(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
logger.error("CONTROLLER ASPECT: EXCEPTION IN METHOD -> " +
exception.getClass());
}
}
Aspect 工作正常,但不幸的是我无法对其进行测试。我尝试了很多次,但在控制器中模拟异常后无法捕获是否调用了 Aspect 方法
@SuppressWarnings("ALL")
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
public class ControllerExceptionAspectTest {
@Autowired
ApplicationContext applicationContext;
@Test
public void testControllerExceptionAspectGetsExecutedWhenExceptionOccures(){
HomeController homeController = (HomeController)applicationContext.getAutowireCapableBeanFactory().getBean("homeController");
try{homeController.callMethod("00000");}
catch (Exception e){}
ControllerExceptionAspect controllerExceptionAspect = (ControllerExceptionAspect)applicationContext.getAutowireCapableBeanFactory().getBean("controllerExceptionAspect");
// HOW TO CATCH THAT ASPECT METHOD WAS CALLED???
}
}
我认为您想要实现的是测试您创建的配置(方面切入点),而不是可以进行单元测试的方面本身。恐怕没有简单的方法可以实现。
您可以遵循一些关于捕获日志或其他想法的互联网建议。老实说,只有当您真的需要测试它是否被调用时,我才会测试方面的预期行为。如果它正在登录,我不会这样做。如果它正在为数据库设置某些内容(或其他副作用),我会验证该值是否在数据库中。那是集成测试的草率基础。
如果您真的、真的必须按照您想要的方式测试方面,您可以编写与给定代码类似的内容。但请记住,正常(非测试)运行时 spring 配置需要存在于 Spring 上下文中的 Verifier 接口的虚拟实现。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Config.class)
public class AspectTesting {
@Autowired
ServiceWithAspect service;
@Autowired
Verifier verifyingAspect;
@Test
public void test() {
// given
boolean condition = false;
// when
try {
service.doit();
} catch (Exception swallow) {}
// then
try {
condition = ((VerifyingAspect) ((Advised) verifyingAspect).getTargetSource().getTarget()).wasExecuted();
} catch (Exception swallow) {}
// then
Assert.assertTrue(condition);
}
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("aspects")
class Config {
}
@Component
class VerifyingAspect implements Verifier {
private boolean executed = false;
public boolean wasExecuted() {
return executed;
}
@Override
public void invoked() {
executed = true;
}
}
@Service
class ServiceWithAspect {
public void doit() {
throw new RuntimeException();
}
}
@Component
@Aspect
class TestedAspect {
@Autowired
Verifier verifier;
@AfterThrowing(pointcut = "execution(* *(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
// your aspect logic here
verifier.invoked();
}
}
interface Verifier {
void invoked();
}
在没有整个 Web 上下文(或根本没有任何上下文)的情况下,单独测试一个方面(包括其切入点表达式)非常容易。
我将首先尝试给出一个通用示例,而不是 OP 问题中的示例。
假设我们有一个方面,如果方法的第一个参数为 null,则必须抛出异常,否则允许方法调用继续。
它应该只应用于使用我们的自定义 @ThrowOnNullFirstArg
注释进行注释的控制器。
@Aspect
public class ThrowOnNullFirstArgAspect {
@Pointcut("" +
"within(@org.springframework.stereotype.Controller *) || " +
"within(@(@org.springframework.stereotype.Controller *) *)")
private void isController() {}
@Around("isController()")
public Object executeAroundController(ProceedingJoinPoint point) throws Throwable {
throwIfNullFirstArgIsPassed(point);
return point.proceed();
}
private void throwIfNullFirstArgIsPassed(ProceedingJoinPoint point) {
if (!(point.getSignature() instanceof MethodSignature)) {
return;
}
if (point.getArgs().length > 0 && point.getArgs()[0] == null) {
throw new IllegalStateException("The first argument is not allowed to be null");
}
}
}
我们可以这样测试:
public class ThrowOnNullFirstArgAspectTest {
private final ThrowOnNullFirstArgAspect aspect = new ThrowOnNullFirstArgAspect();
private TestController controllerProxy;
@Before
public void setUp() {
AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new TestController());
aspectJProxyFactory.addAspect(aspect);
DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
AopProxy aopProxy = proxyFactory.createAopProxy(aspectJProxyFactory);
controllerProxy = (TestController) aopProxy.getProxy();
}
@Test
public void whenInvokingWithNullFirstArg_thenExceptionShouldBeThrown() {
try {
controllerProxy.someMethod(null);
fail("An exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), is("The first argument is not allowed to be null"));
}
}
@Test
public void whenInvokingWithNonNullFirstArg_thenNothingShouldBeThrown() {
String result = controllerProxy.someMethod(Descriptor.builder().externalId("id").build());
assertThat(result, is("ok"));
}
@Controller
@ThrowOnNullFirstArg
private static class TestController {
@SuppressWarnings("unused")
String someMethod(Descriptor descriptor) {
return "ok";
}
}
}
关键部分在setUp()
方法里面。请注意,它还允许验证切入点表达式的正确性。
如何测试切面方法是否真的被调用了?
如果切面方法只有一些难以在测试中验证的效果,您可以使用像 Mockito 这样的模拟库并围绕您的真实切面制作一个存根,然后验证该方法是否被实际调用。
private ControllerExceptionAspect aspect = Mockito.stub(new ControllerExceptionAspect());
然后在你的测试中,通过代理调用控制器后
Mockito.verify(aspect).afterThrowingAdvice(Matchers.any());
如何测试方面方法是否实际写入日志?
如果您正在使用 logback-classic,您可以编写一个 Appender
实现并将其添加到感兴趣的 Logger
,然后检查是否记录了您期望的消息或不是。
public class TestAppender extends AppenderBase<ILoggingEvent> {
public List<ILoggingEvent> events = new ArrayList<>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
}
在夹具设置中:
appender = new TestAppender();
// logback Appender must be started to accept messages
appender.start();
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ControllerExceptionAspect.class.class);
logger.addAppender(appender);
在你的测试中:
List<ILoggingEvent> errors = appender.events.stream()
.filter(event -> event.getLevel() == Level.ERROR)
.collect(Collectors.toList());
assertEquals("Exactly one ERROR is expected in log", 1, errors.size());
// any other assertions you need
可能您还需要 stop()
@After
方法中的 Appender
,但我不确定。
我有一个 Spring MVC 应用程序,我在其中使用方面来捕获所有控制器方法中的异常
@Component
@Aspect
public class ControllerExceptionAspect {
private Logger logger;
public ControllerExceptionAspect() {
logger = Logger.getLogger(ControllerExceptionAspect.class);
}
public ControllerExceptionAspect(Logger logger) {
this.logger = logger;
}
// Catching all exceptions from all methods in all controllers classes
@AfterThrowing(pointcut = "execution(* com.my.package..controller..*(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
logger.error("CONTROLLER ASPECT: EXCEPTION IN METHOD -> " +
exception.getClass());
}
}
Aspect 工作正常,但不幸的是我无法对其进行测试。我尝试了很多次,但在控制器中模拟异常后无法捕获是否调用了 Aspect 方法
@SuppressWarnings("ALL")
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
public class ControllerExceptionAspectTest {
@Autowired
ApplicationContext applicationContext;
@Test
public void testControllerExceptionAspectGetsExecutedWhenExceptionOccures(){
HomeController homeController = (HomeController)applicationContext.getAutowireCapableBeanFactory().getBean("homeController");
try{homeController.callMethod("00000");}
catch (Exception e){}
ControllerExceptionAspect controllerExceptionAspect = (ControllerExceptionAspect)applicationContext.getAutowireCapableBeanFactory().getBean("controllerExceptionAspect");
// HOW TO CATCH THAT ASPECT METHOD WAS CALLED???
}
}
我认为您想要实现的是测试您创建的配置(方面切入点),而不是可以进行单元测试的方面本身。恐怕没有简单的方法可以实现。
您可以遵循一些关于捕获日志或其他想法的互联网建议。老实说,只有当您真的需要测试它是否被调用时,我才会测试方面的预期行为。如果它正在登录,我不会这样做。如果它正在为数据库设置某些内容(或其他副作用),我会验证该值是否在数据库中。那是集成测试的草率基础。
如果您真的、真的必须按照您想要的方式测试方面,您可以编写与给定代码类似的内容。但请记住,正常(非测试)运行时 spring 配置需要存在于 Spring 上下文中的 Verifier 接口的虚拟实现。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Config.class)
public class AspectTesting {
@Autowired
ServiceWithAspect service;
@Autowired
Verifier verifyingAspect;
@Test
public void test() {
// given
boolean condition = false;
// when
try {
service.doit();
} catch (Exception swallow) {}
// then
try {
condition = ((VerifyingAspect) ((Advised) verifyingAspect).getTargetSource().getTarget()).wasExecuted();
} catch (Exception swallow) {}
// then
Assert.assertTrue(condition);
}
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("aspects")
class Config {
}
@Component
class VerifyingAspect implements Verifier {
private boolean executed = false;
public boolean wasExecuted() {
return executed;
}
@Override
public void invoked() {
executed = true;
}
}
@Service
class ServiceWithAspect {
public void doit() {
throw new RuntimeException();
}
}
@Component
@Aspect
class TestedAspect {
@Autowired
Verifier verifier;
@AfterThrowing(pointcut = "execution(* *(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
// your aspect logic here
verifier.invoked();
}
}
interface Verifier {
void invoked();
}
在没有整个 Web 上下文(或根本没有任何上下文)的情况下,单独测试一个方面(包括其切入点表达式)非常容易。
我将首先尝试给出一个通用示例,而不是 OP 问题中的示例。
假设我们有一个方面,如果方法的第一个参数为 null,则必须抛出异常,否则允许方法调用继续。
它应该只应用于使用我们的自定义 @ThrowOnNullFirstArg
注释进行注释的控制器。
@Aspect
public class ThrowOnNullFirstArgAspect {
@Pointcut("" +
"within(@org.springframework.stereotype.Controller *) || " +
"within(@(@org.springframework.stereotype.Controller *) *)")
private void isController() {}
@Around("isController()")
public Object executeAroundController(ProceedingJoinPoint point) throws Throwable {
throwIfNullFirstArgIsPassed(point);
return point.proceed();
}
private void throwIfNullFirstArgIsPassed(ProceedingJoinPoint point) {
if (!(point.getSignature() instanceof MethodSignature)) {
return;
}
if (point.getArgs().length > 0 && point.getArgs()[0] == null) {
throw new IllegalStateException("The first argument is not allowed to be null");
}
}
}
我们可以这样测试:
public class ThrowOnNullFirstArgAspectTest {
private final ThrowOnNullFirstArgAspect aspect = new ThrowOnNullFirstArgAspect();
private TestController controllerProxy;
@Before
public void setUp() {
AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new TestController());
aspectJProxyFactory.addAspect(aspect);
DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
AopProxy aopProxy = proxyFactory.createAopProxy(aspectJProxyFactory);
controllerProxy = (TestController) aopProxy.getProxy();
}
@Test
public void whenInvokingWithNullFirstArg_thenExceptionShouldBeThrown() {
try {
controllerProxy.someMethod(null);
fail("An exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), is("The first argument is not allowed to be null"));
}
}
@Test
public void whenInvokingWithNonNullFirstArg_thenNothingShouldBeThrown() {
String result = controllerProxy.someMethod(Descriptor.builder().externalId("id").build());
assertThat(result, is("ok"));
}
@Controller
@ThrowOnNullFirstArg
private static class TestController {
@SuppressWarnings("unused")
String someMethod(Descriptor descriptor) {
return "ok";
}
}
}
关键部分在setUp()
方法里面。请注意,它还允许验证切入点表达式的正确性。
如何测试切面方法是否真的被调用了?
如果切面方法只有一些难以在测试中验证的效果,您可以使用像 Mockito 这样的模拟库并围绕您的真实切面制作一个存根,然后验证该方法是否被实际调用。
private ControllerExceptionAspect aspect = Mockito.stub(new ControllerExceptionAspect());
然后在你的测试中,通过代理调用控制器后
Mockito.verify(aspect).afterThrowingAdvice(Matchers.any());
如何测试方面方法是否实际写入日志?
如果您正在使用 logback-classic,您可以编写一个 Appender
实现并将其添加到感兴趣的 Logger
,然后检查是否记录了您期望的消息或不是。
public class TestAppender extends AppenderBase<ILoggingEvent> {
public List<ILoggingEvent> events = new ArrayList<>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
}
在夹具设置中:
appender = new TestAppender();
// logback Appender must be started to accept messages
appender.start();
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ControllerExceptionAspect.class.class);
logger.addAppender(appender);
在你的测试中:
List<ILoggingEvent> errors = appender.events.stream()
.filter(event -> event.getLevel() == Level.ERROR)
.collect(Collectors.toList());
assertEquals("Exactly one ERROR is expected in log", 1, errors.size());
// any other assertions you need
可能您还需要 stop()
@After
方法中的 Appender
,但我不确定。