Files
hibernate-demo/src/test/java/com/ankurm/hibernatedemo/jndi/CrossTestPollutionTest.java
T

88 lines
4.0 KiB
Java
Executable File

package com.ankurm.hibernatedemo.jndi;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameAlreadyBoundException;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Backs docs/10-mocking-jndi-datasources.md, chapter "'Name already bound' across two tests in the same JVM".
* Simple-JNDI's {@code MemoryContextFactory}, with {@code org.osjava.sj.jndi.shared=true},
* backs its bindings with a single JVM-wide static map (see
* {@code JndiDataSourceResolutionTest}'s javadoc). If test A binds a name and never unbinds it,
* test B's attempt to bind the SAME name in the SAME JVM fails with
* {@code NameAlreadyBoundException} -- even though the two tests share no code, no fixture, no
* Spring context. {@link MethodOrderer.OrderAnnotation} pins the order here deliberately, so the
* pollution is forced to happen rather than left to JUnit's default (effectively random)
* ordering.
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CrossTestPollutionTest {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
private static final String SHARED_NAME = "jdbc/SharedAcrossTests";
static {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.MemoryContextFactory");
System.setProperty("org.osjava.sj.jndi.shared", "true");
}
@Test
@Order(1)
void testA_bindsAName_andDeliberatelyDoesNotCleanUp() throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:pollution-a;DB_CLOSE_DELAY=-1");
Context ctx = new InitialContext();
ctx.createSubcontext("jdbc");
ctx.bind(SHARED_NAME, ds);
DEMO.info("testA bound {} -- no @AfterEach unbind on purpose, to force the pollution", SHARED_NAME);
// If this assertion passes, the bind succeeded.
assertThat(new InitialContext().lookup(SHARED_NAME)).isNotNull();
}
@Test
@Order(2)
void testB_bindingTheSameNameAgain_throwsNameAlreadyBoundException() throws Exception {
JdbcDataSource ds2 = new JdbcDataSource();
ds2.setURL("jdbc:h2:mem:pollution-b;DB_CLOSE_DELAY=-1");
Context ctx = new InitialContext();
NameAlreadyBoundException ex = assertThrows(NameAlreadyBoundException.class,
() -> ctx.bind(SHARED_NAME, ds2));
DEMO.info("testB's bind() of the SAME name testA left behind failed verbatim with: {}: {}",
ex.getClass().getName(), ex.getMessage());
// The fix: rebind() instead of bind() is idempotent for this exact failure mode --
// it replaces whatever is there instead of throwing.
ctx.rebind(SHARED_NAME, ds2);
Object rebound = new InitialContext().lookup(SHARED_NAME);
assertThat(rebound).isSameAs(ds2);
DEMO.info("ctx.rebind() instead of ctx.bind() -- the standard fix -- succeeded: {}", rebound);
}
@Test
@Order(3)
void testC_theRealFix_isAfterEachUnbindNotRebind() throws Exception {
// rebind() papers over the symptom but each test still leaks its binding into the next
// one's JNDI namespace unless something cleans up. The actual fix is symmetry: whatever
// a test binds, that same test unbinds, in @AfterEach, unconditionally.
try {
new InitialContext().unbind(SHARED_NAME);
DEMO.info("cleanup: unbound {} so it does not leak into any test that runs after this class", SHARED_NAME);
} catch (Exception e) {
DEMO.info("cleanup: nothing to unbind ({})", e.toString());
}
assertThrows(javax.naming.NameNotFoundException.class,
() -> new InitialContext().lookup(SHARED_NAME));
}
}