Skip to content

False assertions when running tests in parallel #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MosheElisha opened this issue Mar 17, 2020 · 2 comments
Closed

False assertions when running tests in parallel #1

MosheElisha opened this issue Mar 17, 2020 · 2 comments

Comments

@MosheElisha
Copy link

MosheElisha commented Mar 17, 2020

Hi,

First, I would like to thank you for this project. It is nicely implemented and very useful.

I would like to report an issue when running the tests in parallel with the following properties:

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

Attaching a small project to reproduce - parallel-tests.zip

If I uncomment the @Execution(ExecutionMode.SAME_THREAD) on the ParallelTest class or use @ResourceLock("LogCapturer") on both test methods, everything works.

Run the tests by executing:

mvn clean test

BTW - I used Flogger which uses slf4j to log4j2

Thanks!

@netmikey
Copy link
Owner

After digging into this a bit, I'm afraid parallel execution won't work for logunit because of the very nature of how logging frameworks work.

Let me explain: logunit works by injecting its own appender, configured with all loggers specified in the LogCapturer.create() call, into the logging framework's configuration before a test method. Those loggers and the appender are removed from the logging framework after that test method, re-added for the next one and so on. This ensures a clean context for each test, even though the logging framework, by nature, is global. Indeed, since logging frameworks are not instantiated per-thread, or even per-test-method but globally for the VM, running logunit in parallel will constantly overwrite the logging framework's configuration, destroying logunit's own context.

The first solution that comes to mind would be redesigning Logunit so that the injection of its appenders and loggers would be handled globally, using some mapping between logger/appender and the test method who asked for them. However, even if we implemented this, you still couldn't tell what test method a specific log message came from. It's like a printer that would be getting print jobs from several computers at the same time: even if it manages to print each page correctly, you couldn't tell what computer a specific page came from just by looking at the page.

Here's an example: Let's say you have 2 test methods, each in its own test class and a unit under test like this:

public class Tested {
  private Logger log = LoggerFactory.getLogger(Tested.class);
  public void doSomething(int arg) {
    if(arg > 0) {
      log.info("positive");
    }
  }
}
public class ParallelTest1 {
  @RegisterExtension
  LogCapturer logCapturer = LogCapturer.create().captureForType(Tested.class);
  public void testZero() {
    new Tested().doSomething(0);
    logCapturer.assertDoesNotContain("positive");
  }
}
public class ParallelTest2 {
  @RegisterExtension
  LogCapturer logCapturer = LogCapturer.create().captureForType(Tested.class);
  public void testPositive() {
    new Tested().doSomething(1);
    logCapturer.assertContains("positive");
  }
}

Now, if both of your tests ParallelTest1 and ParallelTest2 are started concurrently by JUnit, your log file will contain:

10:08:57.563 [ForkJoinPool-1-worker-2] INFO  com.example.Tested - positive

Logunit would have no way of knowing what test method emitted this log entry. So depending on timing (did someone say race condition?), testZero() would either fail or succeed because testZero()'s logCapturer could contain the "positive" log event from testPositive().


I guess my advice would be: if logging is so important that you have to cover it in your tests, and if you absolutely need to run them in parallel, then a solution might be to encapsulate the critical log messages into an interface with an implementation delegating to your log framework at runtime, mocking that interface in test and run your expectations on those mocks.

Something like this setup:

public interface CriticalLog {
  public void accountDeleted(int id);
}

public class CriticalLogImpl {
  private Logger log = LoggerFactory.getLogger(CriticalLogImpl.class);
  public void accountDeleted(int id) {
    log.warn("Account " + id + " deleted.");
  }
}

public class AccountController {
  private CriticalLog log; // use e.g. injection
  public void deleteAccount(int id) {
    // ... do something ...
    log.accountDeleted(id);
  }
}

public class AccountControllerTest {
  public void testAccountDeletion(@Mock CriticalLog logMock) {
    AccountController tested = new AccountController();
    tested.setCritialLog(logMock);
    tested.deleteAccount(3);
    Mockito.verify(logMock).accountDeleted(3);
  }
}

Hope this helps!

@MosheElisha
Copy link
Author

Thank you, @netmikey for digging into this issue! I assumed it won't be easy / possible.

There are very few tests in which I need to test the logger. I will go with the approach of using a @ResourceLock. That is completely acceptable for me.

Thank again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants