Core: enabling context-sensitive tests to start modules

core
spring
unit-tests
Tags: #<Tag:0x00007f23e072c6e8> #<Tag:0x00007f23e0733e70> #<Tag:0x00007f23e0733790>

(Dimitri R) #1

Hi all,

Would anyone have any objection to enrich BaseContextSensitiveTest for it to mock started modules? I just tried this locally and it worked well:

public BaseContextSensitiveTest() {
  ...

  Context.setRuntimeProperties(props);
  
  loadCount++;

  for (Module mod : getModulesToStart()) {
    startModule(mod);
  }
}

Where the new overridable getModulesToStart() provides the modules to appear started during the test (and defaults to an empty list in the base class.)

As I was playing around with @OpenmrsProfile(modules = {...}), I found that the above idea would help a lot mimic in tests what actually happens at runtime with ModuleFactory. It basically ensures that the modules are marked as started before the Spring context is initialized.

Cc @darius, @dkayiwa, @wyclif, @raff


Core: @OpenmrsProfile to filter for modules that are absent
How to segregate imports of module being 'aware of' away from Spring wiring?
How to segregate imports of module being 'aware of' away from Spring wiring?
(Daniel Kayiwa) #2

If it fixes your tests and none of the core platform tests are failing, then a ticket and pull request will be awesome. :smile:


(Wyclif Luyima) #3

That’s what @StartModule annotation does, you add it to the test class


(Dimitri R) #4

Ah great, thanks @wyclif for pointing me to this.

So I just added this to my test:

@StartModule({ "org/openmrs/module/include/exti18n-1.0.0.omod" })

(Not sure if the actual path matters here?)

Anyway I can see that ModuleFactory.startedModules is still empty when OpenmrsProfileExcludeFilter.matchOpenmrsProfileAttributes(..) is reached. Am I missing something?

Also this test doesn’t pass:

@StartModule( { "org/openmrs/module/include/exti18n-1.0.0.omod" })
public class MyTest extends BaseModuleContextSensitiveTest { 
  @Test
  public void should() {
    Assert.assertTrue(ModuleFactory.isModuleStarted("exti18n"));
  }
}

(Darius Jazayeri) #5

My recollection is that the @StartModule annotation doesn’t do what you’d intuitively expect…

-Darius (by phone)


(Wyclif Luyima) #6

Can you take a look at the logs and ensure that the module was successfully started?


(Dimitri R) #7

Indeed not, I’m getting this:

ERROR - ModuleUtil.startup(112) |2017-08-28 18:25:47,006| Unable to load module at path:
org/openmrs/module/include/exti18n-1.0.0.omod because no file exists there and it is not found on the classpath. (absolute path tried: /org/openmrs/module/include/exti18n-1.0.0.omod)

So it looks like an actual .omod should be there as a resource…

I would hope it’s not the way it works, I was expecting something more “mock” so to say. Because if one needs to compile and build dummy OMODs for the sake of unit tests, that could provide to be impractical.


(Wyclif Luyima) #8

Yes, you would need to add the omod to the project for it to work and yes I see a downside to it but it can be improved so that the test framework fetches it from the local maven repo.

If you want some sort of mock then you might as well just mock things you need the regular mock way.


(Dimitri R) #9

Also looking at the stack trace, this happens after Spring loading the beans, defeating the purpose.

Are there examples around of mocking static methods/members of ModuleFactory (or anything similar) in context-sensitive tests?


(Wyclif Luyima) #10

Spring bean loading can happen as many times as possible as long as the context gets refreshed which is done after loading the test modules.

A quick google search on how to mock static methods should give you some good resources.


(Dimitri R) #11

Until now I was loading an actual OMOD without using the annotation that you pointed me to (see here). I will check again to confirm whether @OpenmrsProfile processes this as expected or not.

But I would favour something a lot simpler, like overriding a method of a unit test when needed; such as:

@Override
protected List<Module> getModulesToStart() {
  return Arrays.asList( new Module("", "exti18n", "", "", "", "1.0.0") );
}

(Dimitri R) #12

@wyclif, it looks like one’s got to choose. The test is run either with SpringJUnit4ClassRunner or with PowerMockRunner. Alternatively this kind of thing is possible:

@PowerMockRunnerDelegate(PowerMockRunner.class)
@PrepareForTest(ModuleFactory.class)
public class MyTest extends BaseModuleContextSensitiveTest {
  @Before
  public void setup() {
    mockStatic(ModuleFactory.class);
    when(ModuleFactory.isModuleStarted(any(String.class))).thenReturn(true);
  }

  @Test
  public void should() {
    Assert.assertTrue(ModuleFactory.isModuleStarted("mymodule"));
  }
}

However this would require upgrading powermock-module-junit4 in Core:

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4</artifactId>
  <version>1.6.2</version>
</dependency>

@dkayiwa, I think what could benefit a wider audience, is to expose an overridable method in BaseContextSensitiveTest that would let developers perform some pre-Spring loading actions. Some sort of

protected void setupBeforeContext() {
}

And that method would be called last in its constructor.

Thoughts?


(Daniel Kayiwa) #13

@mksd if you test it out and it proves to work well for you, a pull request will be awesome! :slight_smile:


(Dimitri R) #14

Just trying to brainstorm a little before the fact. Perhaps is there already a way to intercept moments before Spring loading? In which case the PR would be useless.


(Wyclif Luyima) #15

@mksd you should be able to override and use a newer version of a dependency in a module


(Dimitri R) #16

I eventually settled on a very simple solution consisting in “touching” the started modules map of ModuleFactory in the constructor of my context-sensitive test:

public class MyTest extends BaseModuleContextSensitiveTest {
  /*
   * pre-Spring loading setup
   */
  public MyTest() {
    super();
    ModuleFactory.getStartedModulesMap()
      .put("key", new Module("", "moduleid", "", "", "", "1.0.0") );
  }
  ...
}

This has required to mark some key tests with the Spring test annotation @DirtiesContext in order to make sure that they start from a clean slate. For reference, I will report back the relevant commit here when things have been PR’d.


(Dimitri R) #17

@wyclif following your remark I dug further and even though this instruction is called in StartModuleExecutionListener, the conditional bean is not reloaded. The context:

  • A component class has an @OpenmrsProfile annotation with a condition on a module.
  • A unit test starts the module with @StartModule.

This is what happens:

  1. OpenmrsProfileExcludeFilter is called first loading the bean “conditionally”, but at that point the module is not started yet.
  2. The module is started.
  3. ctx.refresh() is called in StartModuleExecutionListener.
  4. OpenmrsProfileExcludeFilter is not revisited and the bean depending on the condition on the module remains loaded as in 1.

Any hints, is this a bug?


(Wyclif Luyima) #18

It’s possible that it’s a bug given that conditional resource loading was added at a later point and possibly the StartModuleExecutionListener was never updated accordingly.


(Dimitri R) #19

Whilst I’m working on TRUNK-5213 I could perhaps have a look at this. Could you point me to the right direction?

I guess my question is: what is it that would trigger OpenmrsProfileExcludeFilter to be called again after a module is started?


(Dimitri R) #20

Ok I made some progress here…

@dkayiwa, would it be possible for you to look at our branch TRUNK-5213 (here) and tell me why this one test doesn’t pass: OpenmrsProfileExcludeFilterWithModulesTest?

For you reference, here are the key classes/files that I have changed and why:

  • TestingApplicationContext.xml so that it can be reloaded within tests to perform a ‘component scan’, here.
  • StartModuleExecutionListener that triggers the component scan prior to refreshing the context, here.
  • OpenmrsProfileWithoutTest1Module, the conditional resource, here.
  • OpenmrsProfileExcludeFilterWithModulesTest, the new (failing) test that starts the module on which there is a condition, here.

I can see that everything seems to happen in order, except that at the end of the day, the conditional bean can still be fetched from the context…

Any help/directions would be greatly appreciated.