Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Live TestMode and Skip Recording Concepts #6671

Merged
Merged
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