Skip to content

Commit

Permalink
Add Live TestMode and Skip Recording Concepts (#6671)
Browse files Browse the repository at this point in the history
* Added TestMode Live, DoNotRecord annotation, and TestContextManager

* Fix linting issue

* Updated new constructors to take TestContextManager instead of splayed parameters

* Updated unit tests based on code changes

* Add missing dependency comment

* Fixing linting issues
  • Loading branch information
alzimmermsft authored and xseeseesee committed Dec 10, 2019
1 parent 62ec799 commit 68679bf
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 79 deletions.
6 changes: 6 additions & 0 deletions sdk/core/azure-core-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<version>5.4.2</version> <!-- {x-version-update;org.junit.jupiter:junit-jupiter-engine;external_dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.4.2</version> <!-- {x-version-update;org.junit.jupiter:junit-jupiter-params;external_dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
* A class that keeps track of network calls by either reading the data from an existing test session record or
* recording the network calls in memory. Test session records are saved or read from:
* "<i>session-records/{@code testName}.json</i>"
* recording the network calls in memory. Test session records are saved or read from: "<i>session-records/{@code
* testName}.json</i>"
*
* <ul>
* <li>If the {@code testMode} is {@link TestMode#PLAYBACK}, the manager tries to find an existing test session
* record to read network calls from.</li>
* <li>If the {@code testMode} is {@link TestMode#RECORD}, the manager creates a new test session record and saves
* all the network calls to it.</li>
* <li>If the {@code testMode} is {@link TestMode#LIVE}, the manager won't attempt to read or create a test session
* record.</li>
* </ul>
*
* When the {@link InterceptorManager} is disposed, if the {@code testMode} is {@link TestMode#RECORD}, the network
Expand All @@ -41,6 +44,8 @@ public class InterceptorManager implements AutoCloseable {
private final Map<String, String> textReplacementRules;
private final String testName;
private final TestMode testMode;
private final boolean allowedToReadRecordedValues;
private final boolean allowedToRecordValues;

// Stores a map of all the HTTP properties in a session
// A state machine ensuring a test is always reset before another one is setup
Expand All @@ -60,44 +65,104 @@ public class InterceptorManager implements AutoCloseable {
*
* @param testName Name of the test session record.
* @param testMode The {@link TestMode} for this interceptor.
* @throws IOException If {@code testMode} is {@link TestMode#PLAYBACK} and an existing test session record could
* not be located or the data could not be deserialized into an instance of {@link RecordedData}.
* @throws UncheckedIOException If {@code testMode} is {@link TestMode#PLAYBACK} and an existing test session record
* could not be located or the data could not be deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} is {@code null}.
* @deprecated Use {@link #InterceptorManager(TestContextManager)} instead.
*/
public InterceptorManager(String testName, TestMode testMode) throws IOException {
@Deprecated
public InterceptorManager(String testName, TestMode testMode) {
this(testName, testMode, false);
}

/**
* Creates a new InterceptorManager that either replays test-session records or saves them.
*
* <ul>
* <li>If {@code testMode} is {@link TestMode#PLAYBACK}, the manager tries to find an existing test session
* record to read network calls from.</li>
* <li>If {@code testMode} is {@link TestMode#RECORD}, the manager creates a new test session record and saves
* all the network calls to it.</li>
* <li>If {@code testMode} is {@link TestMode#LIVE}, the manager won't attempt to read or create a test session
* record.</li>
* </ul>
*
* The test session records are persisted in the path: "<i>session-records/{@code testName}.json</i>"
*
* @param testContextManager Contextual information about the test being ran, such as test name, {@link TestMode},
* and others.
* @throws UncheckedIOException If {@code testMode} is {@link TestMode#PLAYBACK} and an existing test session record
* could not be located or the data could not be deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} is {@code null}.
*/
public InterceptorManager(TestContextManager testContextManager) {
this(testContextManager.getTestName(), testContextManager.getTestMode(), testContextManager.doNotRecordTest());
}

private InterceptorManager(String testName, TestMode testMode, boolean doNotRecord) {
Objects.requireNonNull(testName, "'testName' cannot be null.");

this.testName = testName;
this.testMode = testMode;
this.textReplacementRules = new HashMap<>();

this.recordedData = testMode == TestMode.PLAYBACK
? readDataFromFile()
: new RecordedData();
this.allowedToReadRecordedValues = (testMode == TestMode.PLAYBACK && !doNotRecord);
this.allowedToRecordValues = (testMode == TestMode.RECORD && !doNotRecord);

if (allowedToReadRecordedValues) {
this.recordedData = readDataFromFile();
} else if (allowedToRecordValues) {
this.recordedData = new RecordedData();
} else {
this.recordedData = null;
}
}

/**
* Creates a new InterceptorManager that replays test session records. It takes a set of
* {@code textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a
* {@link NetworkCallRecord#getResponse()}.
* {@code textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* NetworkCallRecord#getResponse()}.
*
* The test session records are read from: "<i>session-records/{@code testName}.json</i>"
*
* @param testName Name of the test session record.
* @param textReplacementRules A set of rules to replace text in {@link NetworkCallRecord#getResponse()} when playing
* back network calls.
* @throws IOException An existing test session record could not be located or the data could not be deserialized
* into an instance of {@link RecordedData}.
* @param textReplacementRules A set of rules to replace text in {@link NetworkCallRecord#getResponse()} when
* playing back network calls.
* @throws UncheckedIOException An existing test session record could not be located or the data could not be
* deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} or {@code textReplacementRules} is {@code null}.
* @deprecated Use {@link #InterceptorManager(String, Map, boolean)} instead.
*/
public InterceptorManager(String testName, Map<String, String> textReplacementRules) throws IOException {
@Deprecated
public InterceptorManager(String testName, Map<String, String> textReplacementRules) {
this(testName, textReplacementRules, false);
}

/**
* Creates a new InterceptorManager that replays test session records. It takes a set of
* {@code textReplacementRules}, that can be used by {@link PlaybackClient} to replace values in a {@link
* NetworkCallRecord#getResponse()}.
*
* The test session records are read from: "<i>session-records/{@code testName}.json</i>"
*
* @param testName Name of the test session record.
* @param textReplacementRules A set of rules to replace text in {@link NetworkCallRecord#getResponse()} when
* playing back network calls.
* @param doNotRecord Flag indicating whether network calls should be record or played back.
* @throws UncheckedIOException An existing test session record could not be located or the data could not be
* deserialized into an instance of {@link RecordedData}.
* @throws NullPointerException If {@code testName} or {@code textReplacementRules} is {@code null}.
*/
public InterceptorManager(String testName, Map<String, String> textReplacementRules, boolean doNotRecord) {
Objects.requireNonNull(testName, "'testName' cannot be null.");
Objects.requireNonNull(textReplacementRules, "'textReplacementRules' cannot be null.");

this.testName = testName;
this.testMode = TestMode.PLAYBACK;
this.allowedToReadRecordedValues = !doNotRecord;
this.allowedToRecordValues = false;

this.recordedData = readDataFromFile();
this.recordedData = allowedToReadRecordedValues ? readDataFromFile() : null;
this.textReplacementRules = textReplacementRules;
}

Expand All @@ -120,7 +185,8 @@ public RecordedData getRecordedData() {
}

/**
* Gets a new HTTP pipeline policy that records network calls and its data is managed by {@link InterceptorManager}.
* Gets a new HTTP pipeline policy that records network calls and its data is managed by {@link
* InterceptorManager}.
*
* @return HttpPipelinePolicy to record network calls.
*/
Expand All @@ -145,27 +211,24 @@ public HttpClient getPlaybackClient() {
*/
@Override
public void close() {
switch (testMode) {
case RECORD:
try {
writeDataToFile();
} catch (IOException e) {
logger.error("Unable to write data to playback file.", e);
}
break;
case PLAYBACK:
// Do nothing
break;
default:
logger.error("==> Unknown AZURE_TEST_MODE: {}", testMode);
break;
if (allowedToRecordValues) {
try {
writeDataToFile();
} catch (IOException e) {
logger.error("Unable to write data to playback file.", e);
}
}
}

private RecordedData readDataFromFile() throws IOException {
private RecordedData readDataFromFile() {
File recordFile = getRecordFile(testName);
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
return mapper.readValue(recordFile, RecordedData.class);

try {
return mapper.readValue(recordFile, RecordedData.class);
} catch (IOException ex) {
throw logger.logExceptionAsWarning(new UncheckedIOException(ex));
}
}

/*
Expand All @@ -183,10 +246,11 @@ private File getRecordFile(String testName) {
File playbackFile = new File(getRecordFolder(), testName + ".json");

if (!playbackFile.exists()) {
throw logger.logExceptionAsError(new RuntimeException(String.format(
"Missing playback file. File path: %s. ", playbackFile)));
throw logger.logExceptionAsError(new RuntimeException(String.format(
"Missing playback file. File path: %s. ", playbackFile.getPath())));
}
logger.info("==> Playback file path: " + playbackFile);

logger.info("==> Playback file path: " + playbackFile.getPath());
return playbackFile;
}

Expand Down Expand Up @@ -218,7 +282,8 @@ private File createRecordFile(String testName) throws IOException {
}

/**
* Add text replacement rule (regex as key, the replacement text as value) into {@link InterceptorManager#textReplacementRules}
* Add text replacement rule (regex as key, the replacement text as value) into {@link
* InterceptorManager#textReplacementRules}
*
* @param regex the pattern to locate the position of replacement
* @param replacement the replacement text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
// Licensed under the MIT License.
package com.azure.core.test;

import com.azure.core.util.Configuration;
import com.azure.core.test.utils.TestResourceNamer;
import com.azure.core.util.Configuration;
import com.azure.core.util.logging.ClientLogger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Locale;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.util.Locale;

/**
* Base class for running live and playback tests using {@link InterceptorManager}.
*/
Expand All @@ -29,6 +29,8 @@ public abstract class TestBase implements BeforeEachCallback {

protected InterceptorManager interceptorManager;
protected TestResourceNamer testResourceNamer;
protected TestContextManager testContextManager;

private ExtensionContext extensionContext;

/**
Expand All @@ -53,16 +55,16 @@ public void beforeEach(ExtensionContext extensionContext) {
*/
@BeforeEach
public void setupTest(TestInfo testInfo) {
final String testName = testInfo.getTestMethod().get().getName();
logger.info("Test Mode: {}, Name: {}", testMode, testName);
this.testContextManager = new TestContextManager(testInfo.getTestMethod().get(), testMode);
logger.info("Test Mode: {}, Name: {}", testMode, testContextManager.getTestName());

try {
interceptorManager = new InterceptorManager(testName, testMode);
} catch (IOException e) {
logger.error("Could not create interceptor for {}", testName, e);
interceptorManager = new InterceptorManager(testContextManager);
} catch (UncheckedIOException e) {
logger.error("Could not create interceptor for {}", testContextManager.getTestName(), e);
Assertions.fail();
}
testResourceNamer = new TestResourceNamer(testName, testMode, interceptorManager.getRecordedData());
testResourceNamer = new TestResourceNamer(testContextManager, interceptorManager.getRecordedData());

beforeTest();
}
Expand All @@ -73,8 +75,10 @@ public void setupTest(TestInfo testInfo) {
*/
@AfterEach
public void teardownTest(TestInfo testInfo) {
afterTest();
interceptorManager.close();
if (testContextManager.didTestRun()) {
afterTest();
interceptorManager.close();
}
}

/**
Expand Down Expand Up @@ -133,4 +137,22 @@ private static TestMode initializeTestMode() {
logger.info("Environment variable '{}' has not been set yet. Using 'Playback' mode.", AZURE_TEST_MODE);
return TestMode.PLAYBACK;
}

/**
* Sleeps the test for the given amount of milliseconds if {@link TestMode} isn't {@link TestMode#PLAYBACK}.
*
* @param millis Number of milliseconds to sleep the test.
* @throws IllegalStateException If the sleep is interrupted.
*/
protected void sleepIfRunningAgainstService(long millis) {
if (testMode == TestMode.PLAYBACK) {
return;
}

try {
Thread.sleep(millis);
} catch (InterruptedException ex) {
throw logger.logExceptionAsWarning(new IllegalStateException(ex));
}
}
}
Loading

0 comments on commit 68679bf

Please sign in to comment.