How to fake InitialContext with default constructo

2020-05-23 06:22发布

问题:

All,

I'm trying to do some unit testing in some archaic java code (no interfaces, no abstraction, etc.)

This is a servlet that uses a ServletContext (which I'm assuming is set up by Tomcat) and it has database information is set up in the web.xml/context.xml file. Now, I've figured out how to make a Fake ServletContext, but the code has

 InitialContext _ic = new InitialContext();

all over the place (so it isn't feasible to replace it). I need to find a way to make a default InitialContext() able to do the _ic.lookup(val) without throwing an exception.

I'm assuming there is some way that the context.xml is getting loaded, but how that magic works, I'm drawing a blank. Anyone have any ideas?

回答1:

You can use PowerMock to mock construction of the InitialContext and control its behavior. Constructor Mocking is documented here.

PowerMock tests can be quite messy and complicated, refactoring is normally a better option.



回答2:

Take advantage of the fact that InitialContext uses an SPI to handle its creation. You can hook into its lifecycle by creating an implementation of javax.naming.spi.InitialContextFactory and passing that to your tests via the system property javax.naming.factory.initial (Context.INTITIAL_CONTEXT_FACTORY). It's simpler than it sounds.

Given this class:

public class UseInitialContext {

    public UseInitialContext() {
        try {
            InitialContext ic = new InitialContext();
            Object myObject = ic.lookup("myObject");
            System.out.println(myObject);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }


} 

And this impl of InitialContextFactory:

public class MyInitialContextFactory implements InitialContextFactory {

    public Context getInitialContext(Hashtable<?, ?> arg0)
            throws NamingException {

        Context context = Mockito.mock(Context.class);
        Mockito.when(context.lookup("myObject")).thenReturn("This is my object!!");
        return context;
    }
}

Creating an instance of UseInitialContext in a junit test with

-Djava.naming.initial.factory=initial.context.test.MyInitialContext

on the command line outputs This is my object!! (easy to set up in eclipse). I like Mockito for mocking and stubbing. I'd also recommend Micheal Feather's Working Effectively with Legacy Code if you deal with lots of legacy code. It's all about how to find seams in programs in order to isolate specific pieces for testing.



回答3:

Here's my solution to setting up the Inintial Context for my unit tests. First I added the following test dependency to my project:

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>catalina</artifactId>
  <version>6.0.33</version>
  <scope>test</scope>
</dependency>

Then I created a static method with the following code:

public static void setupInitialContext() throws Exception {
    System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
    System.setProperty(Context.URL_PKG_PREFIXES, "org.apache.naming");
    InitialContext ic = new InitialContext();
    ic.createSubcontext("jdbc");
    PGSimpleDataSource ds = new PGSimpleDataSource();
    ds.setDatabaseName("postgres");
    ds.setUser("postgres");
    ds.setPassword("admin");
    ic.bind("jdbc/something", ds);
}

Finally in each of my test class I add an @BeforeClass method which calls setupInitialContext.



回答4:

Try setting up the system variables before:

System.setProperty(Context.INITIAL_CONTEXT_FACTORY,
        "org.apache.naming.java.javaURLContextFactory");
System.setProperty(Context.URL_PKG_PREFIXES,
        "org.apache.naming");
InitialContext ic = new InitialContext();

If you are using JUnit, follow this doc: https://blogs.oracle.com/randystuph/entry/injecting_jndi_datasources_for_junit



回答5:

Today I've faced the same problem (we can't user PowerMock) and solved it this way:

  1. Don't lookup in the constructor so when you invoke @InitMock on the object, the constructor doesn't require the context yet.

  2. Create a method for retrieving the service bean when needed like "getService().serviceMethod(param, param ...)":

    /* Class ApplicationResourceProvider */

    /* We can mock this and set it up with InjectMocks */
    InitialContext ic;

    /* method hiding the lookup */
    protected ApplicationService getService() throws NamingException {
        if(ic == null)
            ic = new InitialContext();
        return (ApplicationService)ic.lookup("java:global/defaultApplicationLocal");
    }
  1. On the test, set it up:
@Mock
ApplicationService applicationServiceBean;

@Mock
InitialContext ic;

@InjectMocks
ApplicationResourceProvider arp;

@Before
public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    when(ic.lookup(anyString())).thenReturn(applicationServiceBean);
    ...
}


回答6:

Have you considered mockito?

It's as easy as:

InitialContext ctx = mock(InitialContext.class);

By the way, should you choose to use mocks i would recommend reading this article as well: http://martinfowler.com/articles/mocksArentStubs.html



回答7:

A poor man's standalone implementation using no external libraries:

public class myTestClass {
    public static class TestContext extends InitialContext {
        public TestContext() throws NamingException {
            super(true /*prevents initialization*/);
        }

        static Object someExpectedValue = "the expected string or object instance";

        /*override the method(s) called by the legacy program on _ic, check the parameter and return the wanted value */
        public Object lookup(String name) throws NamingException {
            return name != null && name.equals("theValueOfVal") ? someExpectedValue : null;
        }
    }

    public static class TestInitialContextFactory implements InitialContextFactory {
        public Context getInitialContext(Hashtable<?, ?> arg0) throws NamingException {
            return new TestContext();
        }
    }

    public static void main(String[] args) throws SQLException {
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "the.package.myTestClass$TestInitialContextFactory");
        /*now call the legacy logic to be tested*/
        ...

You could use a switch in the override of the lookup method to return the expected value for each different val value passed to _ic.lookup(val) throughout the legacy program.