Search code examples
junitaop

Logging of unit test result using AspectJ


I'm trying to log my test suite results using AspectJ. I'd like to 'inject' the results identification code after each @Test method in my code, so created an aspect with the following method:

  @After("execution(* *(..)) && @annotation(org.junit.Test)")
  public void afterTestMethod(JoinPoint joinPoint) {
      //identify test result
  }

However, can't find how to retrieve the test method result (passed/failed/skipped). Any suggestions?

Thanks!


Solution

  • A) JUnit run listener

    I am assuming that you build your project with something like Maven or Gradle and will show you a Maven example for a JUnit 4 RunListener:

    In module test-tools you would add

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>
      <!--
        We build something with the JUnit API, not just run a test,
        so scope 'test' is not enough here
      -->
      <scope>compile</scope>
    </dependency>
    

    to your POM and then have something like this in src/main/java/...:

    package de.scrum_master.testing;
    
    import org.junit.runner.Description;
    import org.junit.runner.Result;
    import org.junit.runner.notification.Failure;
    import org.junit.runner.notification.RunListener;
    
    public class ResultPrintingRunListener extends RunListener {
      @Override
      public void testRunStarted(Description description) {
        System.out.println("[RunStarted] description = " + description);
      }
    
      @Override
      public void testRunFinished(Result result) {
        System.out.println("[RunFinished] result = " + result);
        System.out.println("  run count = " + result.getRunCount());
        System.out.println("  failure count = " + result.getFailureCount());
        System.out.println("  assumption failure count = " + result.getAssumptionFailureCount());
        System.out.println("  ignored count = " + result.getIgnoreCount());
        System.out.println("  run time (ms) = " + result.getRunTime());
      }
    
      @Override
      public void testSuiteStarted(Description description) {
        System.out.println("[SuiteStarted] description = " + description);
      }
    
      @Override
      public void testSuiteFinished(Description description) {
        System.out.println("[SuiteFinished] description = " + description);
      }
    
      @Override
      public void testStarted(Description description) {
        System.out.println("[Started] description = " + description);
      }
    
      @Override
      public void testFinished(Description description) {
        System.out.println("[Finished] description = " + description);
      }
    
      @Override
      public void testFailure(Failure failure) {
        System.out.println("[Failure] failure = " + failure);
      }
    
      @Override
      public void testAssumptionFailure(Failure failure) {
        System.out.println("[AssumptionFailure] failure = " + failure);
      }
    
      @Override
      public void testIgnored(Description description) {
        System.out.println("[Ignored] description = " + description);
      }
    }
    

    Instead of logging you would put your API calls in there at the appropriate places.

    In your application test module you would add the test-tools module as a dependency and then configure your Maven Surefire/Failsafe plugins like this:

    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.22.2</version>
      <configuration>
        <properties>
          <property>
            <name>listener</name>
            <value>de.scrum_master.testing.ResultPrintingRunListener</value>
          </property>
        </properties>
      </configuration>
    </plugin>
    

    If then you run a test like this in Maven, ...

    package de.scrum_master.agent.aspect;
    
    import org.junit.Assert;
    import org.junit.Ignore;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.TestWatcher;
    import org.junit.runner.Description;
    
    public class MyTest {
      @Test
      public void one() {
        Assert.assertEquals("xander", "Alexander".substring(3));
      }
    
      @Test
      public void two() {
        Assert.assertEquals("Alex", "Alexander".substring(3));
      }
    
      @Test
      public void three() {
        Assert.assertEquals(11, 1 / 0);
      }
    
      @Test
      @Ignore
      public void four() {
        Assert.assertNull(null);
      }
    }
    

    ... Maven prints:

    [INFO] -------------------------------------------------------
    [INFO]  T E S T S
    [INFO] -------------------------------------------------------
    [RunStarted] description = null
    [INFO] Running de.scrum_master.agent.aspect.MyTest
    [SuiteStarted] description = de.scrum_master.agent.aspect.MyTest
    [Started] description = one(de.scrum_master.agent.aspect.MyTest)
    [Finished] description = one(de.scrum_master.agent.aspect.MyTest)
    [Started] description = two(de.scrum_master.agent.aspect.MyTest)
    [Failure] failure = two(de.scrum_master.agent.aspect.MyTest): expected:<[Alex]> but was:<[xander]>
    [Finished] description = two(de.scrum_master.agent.aspect.MyTest)
    [Ignored] description = four(de.scrum_master.agent.aspect.MyTest)
    [Started] description = three(de.scrum_master.agent.aspect.MyTest)
    [Failure] failure = three(de.scrum_master.agent.aspect.MyTest): / by zero
    [Finished] description = three(de.scrum_master.agent.aspect.MyTest)
    [SuiteFinished] description = de.scrum_master.agent.aspect.MyTest
    [ERROR] Tests run: 4, Failures: 1, Errors: 1, Skipped: 1, Time elapsed: 0.11 s <<< FAILURE! - in de.scrum_master.agent.aspect.MyTest
    [ERROR] de.scrum_master.agent.aspect.MyTest.two  Time elapsed: 0.007 s  <<< FAILURE!
    org.junit.ComparisonFailure: expected:<[Alex]> but was:<[xander]>
        at de.scrum_master.agent.aspect.MyTest.two(MyTest.java:31)
    
    [ERROR] de.scrum_master.agent.aspect.MyTest.three  Time elapsed: 0.001 s  <<< ERROR!
    java.lang.ArithmeticException: / by zero
        at de.scrum_master.agent.aspect.MyTest.three(MyTest.java:36)
    
    [RunFinished] result = org.junit.runner.Result@79be0360
      run count = 3
      failure count = 2
      assumption failure count = 0
      ignored count = 1
      run time (ms) = 0
    

    JUnit test watcher rule

    If you prefer something which is independent of a specific JUnit runner right inside your tests, create a base class like this using JUnit TestWatcher:

    package de.scrum_master.agent.aspect;
    
    import org.junit.Rule;
    import org.junit.rules.TestWatcher;
    import org.junit.runner.Description;
    
    public class TestBase {
      @Rule(order = Integer.MIN_VALUE)
      public TestWatcher testWatcher = new TestWatcher() {
        @Override
        protected void failed(Throwable e, Description description) {
          System.out.println("[TestWatcher failed] description = " + description +", e = " + e);
        }
    
        @Override
        protected void succeeded(Description description) {
          System.out.println("[TestWatcher succeeded] description = " + description);
        }
      };
    }
    

    Then make all your test classes extends TestBase directly or indirectly. If you run the test, e.g. from an IDE, you see (shortened output, only test watcher log):

    [TestWatcher succeeded] description = one(de.scrum_master.agent.aspect.MyTest)
    [TestWatcher failed] description = two(de.scrum_master.agent.aspect.MyTest), e = org.junit.ComparisonFailure: expected:<[Alex]> but was:<[xander]>
    [TestWatcher failed] description = three(de.scrum_master.agent.aspect.MyTest), e = java.lang.ArithmeticException: / by zero
    

    You see, there are fewer events in the test watcher, e.g. ignored tests are not reported.

    As much as I love AspectJ (which is how I found this question), I think you should try configuring JUnit appropriately first and will probably be happy with it. If for whatever reason - please explain, if so - you still insist in an AOP solution, please let me know.


    (C) Intercept JUnit tests directly

    Because you insisted:

    package de.scrum_master.aspectj;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class TestResultInterceptor {
      @AfterReturning(value = "execution(* *(..)) && @annotation(org.junit.Test)", returning = "result")
      public void allMethods(JoinPoint joinPoint, Object result) {
        System.out.println(joinPoint + " -> PASSED");
      }
    
      @AfterThrowing(value = "execution(* *(..)) && @annotation(org.junit.Test)", throwing = "throwable")
      public void allMethods(JoinPoint joinPoint, Throwable throwable) {
        System.out.println(joinPoint + " -> FAILED: " + throwable);
      }
    }
    

    When running the JUnit test above in my IDE, the console log is:

    execution(void de.scrum_master.testing.MyTest.one()) -> PASSED
    execution(void de.scrum_master.testing.MyTest.two()) -> FAILED: org.junit.ComparisonFailure: expected:<[Alex]> but was:<[xander]>
    execution(void de.scrum_master.testing.MyTest.three()) -> FAILED: java.lang.ArithmeticException: / by zero
    

    I think you can take it from here.