Make AppEngineRule work with JUnit 5 (#548)

* Make AppEngineRule work with JUnit 5

Made AppEngineRule work with both JUnit4 and JUnit 5 and applied
it to PremiumListTest.

Next step is to convert SqlIntegrationTestSuite.java to JUnit5.
This commit is contained in:
Weimin Yu 2020-04-07 14:59:25 -04:00 committed by GitHub
parent 2f6ba8cc29
commit 3dbc4e5d46
4 changed files with 97 additions and 15 deletions

View file

@ -29,16 +29,14 @@ import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumList.PremiumListRevision; import google.registry.model.registry.label.PremiumList.PremiumListRevision;
import google.registry.testing.AppEngineRule; import google.registry.testing.AppEngineRule;
import org.joda.money.Money; import org.joda.money.Money;
import org.junit.Rule;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link PremiumList}. */ /** Unit tests for {@link PremiumList}. */
@EnableRuleMigrationSupport
public class PremiumListTest { public class PremiumListTest {
@Rule @RegisterExtension
public final AppEngineRule appEngine = AppEngineRule.builder().withDatastoreAndCloudSql().build(); public final AppEngineRule appEngine = AppEngineRule.builder().withDatastoreAndCloudSql().build();
@BeforeEach @BeforeEach

View file

@ -27,6 +27,7 @@ import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import javax.persistence.Entity; import javax.persistence.Entity;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
@ -51,15 +52,15 @@ public class JpaEntityCoverage extends ExternalResource {
private static final Map<String, Boolean> testsJpaEntities = Maps.newHashMap(); private static final Map<String, Boolean> testsJpaEntities = Maps.newHashMap();
// Provides class name of the test being executed. // Provides class name of the test being executed.
private final TestCaseWatcher watcher; private final Supplier<String> currTestClassNameSupplier;
public JpaEntityCoverage(TestCaseWatcher watcher) { public JpaEntityCoverage(Supplier<String> currTestClassNameSupplier) {
this.watcher = watcher; this.currTestClassNameSupplier = currTestClassNameSupplier;
} }
@Override @Override
public void before() { public void before() {
testsJpaEntities.putIfAbsent(watcher.getTestClass(), false); testsJpaEntities.putIfAbsent(currTestClassNameSupplier.get(), false);
} }
@Override @Override
@ -69,7 +70,7 @@ public class JpaEntityCoverage extends ExternalResource {
.forEach( .forEach(
entity -> { entity -> {
allCoveredJpaEntities.add(entity); allCoveredJpaEntities.add(entity);
testsJpaEntities.put(watcher.getTestClass(), true); testsJpaEntities.put(currTestClassNameSupplier.get(), true);
}); });
} }

View file

@ -29,6 +29,9 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.rules.RuleChain; import org.junit.rules.RuleChain;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.junit.runner.Description; import org.junit.runner.Description;
@ -88,7 +91,7 @@ public class JpaTestRules {
this.ruleChain = this.ruleChain =
RuleChain.outerRule(watcher) RuleChain.outerRule(watcher)
.around(integrationTestRule) .around(integrationTestRule)
.around(new JpaEntityCoverage(watcher)); .around(new JpaEntityCoverage(watcher::getTestClass));
} }
@Override @Override
@ -97,6 +100,40 @@ public class JpaTestRules {
} }
} }
/**
* JUnit extension for member classes of {@link
* google.registry.schema.integration.SqlIntegrationTestSuite}. In addition to providing a
* database through {@link JpaIntegrationTestRule}, it also keeps track of the test coverage of
* the declared JPA entities (in persistence.xml). Per-class statistics are stored in static
* variables. The SqlIntegrationTestSuite inspects the cumulative statistics after all test
* classes have run.
*/
public static final class JpaIntegrationWithCoverageExtension
implements BeforeEachCallback, AfterEachCallback {
private String currentTestClassName = null;
private final JpaEntityCoverage jpaEntityCoverage =
new JpaEntityCoverage(() -> this.currentTestClassName);
private final JpaIntegrationTestRule integrationTestRule;
JpaIntegrationWithCoverageExtension(JpaIntegrationTestRule integrationTestRule) {
this.integrationTestRule = integrationTestRule;
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
this.currentTestClassName = context.getRequiredTestClass().getName();
integrationTestRule.before();
jpaEntityCoverage.before();
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
jpaEntityCoverage.after();
integrationTestRule.after();
this.currentTestClassName = null;
}
}
/** Builder of test rules that provide {@link JpaTransactionManager}. */ /** Builder of test rules that provide {@link JpaTransactionManager}. */
public static class Builder { public static class Builder {
private String initScript; private String initScript;
@ -149,6 +186,15 @@ public class JpaTestRules {
return new JpaIntegrationWithCoverageRule(buildIntegrationTestRule()); return new JpaIntegrationWithCoverageRule(buildIntegrationTestRule());
} }
/**
* JUnit extension that adapts {@link JpaIntegrationTestRule} for JUnit 5 and also checks test
* coverage of JPA entity classes.
*/
public JpaIntegrationWithCoverageExtension buildIntegrationWithCoverageExtension() {
checkState(initScript == null, "Integration tests do not accept initScript");
return new JpaIntegrationWithCoverageExtension(buildIntegrationTestRule());
}
/** Builds a {@link JpaUnitTestRule} instance. */ /** Builds a {@link JpaUnitTestRule} instance. */
public JpaUnitTestRule buildUnitTestRule() { public JpaUnitTestRule buildUnitTestRule() {
checkState( checkState(

View file

@ -14,6 +14,7 @@
package google.registry.testing; package google.registry.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.testing.DatastoreHelper.persistSimpleResources; import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import static google.registry.util.ResourceUtils.readResourceUtf8; import static google.registry.util.ResourceUtils.readResourceUtf8;
@ -40,6 +41,7 @@ import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarContact; import google.registry.model.registrar.RegistrarContact;
import google.registry.persistence.transaction.JpaTestRules; import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule; import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule;
import google.registry.util.Clock; import google.registry.util.Clock;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -53,6 +55,9 @@ import javax.annotation.Nullable;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.runner.Description; import org.junit.runner.Description;
@ -66,9 +71,15 @@ import org.junit.runners.model.Statement;
* *
* <p>This rule also resets global Objectify for the current thread. * <p>This rule also resets global Objectify for the current thread.
* *
* <p>This class works with both JUnit 4 and JUnit 5. With JUnit 4, the test runner calls {@link
* #apply(Statement, Description)}, which in turns calls {@link #before()} on entry and {@link
* #after()} on exit. With JUnit 5, the test runner calls {@link #beforeEach(ExtensionContext)} and
* {@link #afterEach(ExtensionContext)}.
*
* @see org.junit.rules.ExternalResource * @see org.junit.rules.ExternalResource
*/ */
public final class AppEngineRule extends ExternalResource { public final class AppEngineRule extends ExternalResource
implements BeforeEachCallback, AfterEachCallback {
public static final String NEW_REGISTRAR_GAE_USER_ID = "666"; public static final String NEW_REGISTRAR_GAE_USER_ID = "666";
public static final String THE_REGISTRAR_GAE_USER_ID = "31337"; public static final String THE_REGISTRAR_GAE_USER_ID = "31337";
@ -93,6 +104,8 @@ public final class AppEngineRule extends ExternalResource {
/** A rule-within-a-rule to provide a temporary folder for AppEngineRule's internal temp files. */ /** A rule-within-a-rule to provide a temporary folder for AppEngineRule's internal temp files. */
TemporaryFolder temporaryFolder = new TemporaryFolder(); TemporaryFolder temporaryFolder = new TemporaryFolder();
// Sets up a SQL database when running on JUnit 5.
JpaIntegrationWithCoverageExtension jpaIntegrationWithCoverageExtension = null;
private boolean withDatastoreAndCloudSql; private boolean withDatastoreAndCloudSql;
private boolean withLocalModules; private boolean withLocalModules;
@ -257,14 +270,38 @@ public final class AppEngineRule extends ExternalResource {
.build(); .build();
} }
/** Called before every test method. JUnit 5 only. */
@Override
public void beforeEach(ExtensionContext context) throws Exception {
before();
if (withDatastoreAndCloudSql) {
JpaTestRules.Builder builder = new JpaTestRules.Builder();
if (clock != null) {
builder.withClock(clock);
}
jpaIntegrationWithCoverageExtension = builder.buildIntegrationWithCoverageExtension();
jpaIntegrationWithCoverageExtension.beforeEach(context);
}
}
/** Called after each test method. JUnit 5 only. */
@Override
public void afterEach(ExtensionContext context) throws Exception {
if (withDatastoreAndCloudSql) {
checkState(
jpaIntegrationWithCoverageExtension != null, "Null jpaIntegrationWithCoverageExtension");
jpaIntegrationWithCoverageExtension.afterEach(context);
}
after();
}
/** /**
* Hack to make sure AppEngineRule is always wrapped in a {@link JpaIntegrationWithCoverageRule}. * Hack to make sure AppEngineRule is always wrapped in a {@link JpaIntegrationWithCoverageRule}.
* JUnit 4 only.
*/ */
// Note: Even with @EnableRuleMigrationSupport, JUnit5 runner does not call this method. // Note: Even with @EnableRuleMigrationSupport, JUnit5 runner does not call this method.
// Note 2: Do not migrate any members of SqlIntegrationTestSuite to JUnit5 before // Note 2: Do not migrate members of SqlIntegrationTestSuite to JUnit5 individually.
// calls to JpaIntegrationWithCoverageRule can be made elsewhere. // TODO(weiminyu): migrate SqlIntegrationTestSuite in one go.
// TODO(weiminyu): make JpaIntegrationWithCoverageRule implement ExternaResource and invoke it in
// before() and after(), then drop this method.
@Override @Override
public Statement apply(Statement base, Description description) { public Statement apply(Statement base, Description description) {
Statement statement = base; Statement statement = base;