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
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,100 @@ 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(String, TestMode, boolean)} instead.
*/
public InterceptorManager(String testName, TestMode testMode) throws IOException {
@Deprecated
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved
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 testName Name of the test session record.
* @param testMode The {@link TestMode} for this interceptor.
* @param doNotRecord Flag indicating whether network calls should be record or played back.
* @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(String testName, TestMode testMode, boolean doNotRecord) {
Objects.requireNonNull(testName, "'testName' cannot be null.");

this.testName = testName;
this.testMode = testMode;
this.textReplacementRules = new HashMap<>();
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;
}
}

this.recordedData = testMode == TestMode.PLAYBACK
? readDataFromFile()
: new RecordedData();
/**
* 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.
* @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.
*/
@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()}.
* {@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.
* @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) throws IOException {
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved
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 +181,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 +207,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 +242,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 +278,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,33 +2,35 @@
// 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}.
*/
public abstract class TestBase implements BeforeEachCallback {
// Environment variable name used to determine the TestMode.
private static final String AZURE_TEST_MODE = "AZURE_TEST_MODE";
private static TestMode testMode;
static TestMode testMode;

private final ClientLogger logger = new ClientLogger(TestBase.class);

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

private ExtensionContext extensionContext;

/**
Expand All @@ -53,16 +55,21 @@ public void beforeEach(ExtensionContext extensionContext) {
*/
@BeforeEach
public void setupTest(TestInfo testInfo) {
final String testName = testInfo.getTestMethod().get().getName();
final Method testMethod = testInfo.getTestMethod().get();
this.testContextManager = new TestContextManager(testMethod);
testContextManager.verifyTestCanRunInTestMode(testMode);

final String testName = testMethod.getName();
logger.info("Test Mode: {}, Name: {}", testMode, testName);

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

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

/**
Expand Down Expand Up @@ -133,4 +142,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));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.test;

import com.azure.core.test.annotation.DoNotRecord;

import java.lang.reflect.Method;

import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
* This class handles managing context about a test, such as custom testing annotations and verifying whether the test
* is capable of running.
*/
public class TestContextManager {
private final boolean doNotRecord;
private final boolean skipInPlayback;
private volatile boolean testRan;

/**
* Constructs a {@link TestContextManager} based on the test method.
*
* @param testMethod Test method being ran.
*/
public TestContextManager(Method testMethod) {
DoNotRecord doNotRecordAnnotation = testMethod.getAnnotation(DoNotRecord.class);
if (doNotRecordAnnotation != null) {
this.doNotRecord = true;
this.skipInPlayback = doNotRecordAnnotation.skipInPlayback();
} else {
this.doNotRecord = false;
this.skipInPlayback = false;
}
}

/**
* Verifies whether the current test is allowed to run.
*
* @param testMode The {@link TestMode} tests are being ran in.
*/
public void verifyTestCanRunInTestMode(TestMode testMode) {
this.testRan = !(skipInPlayback && testMode == TestMode.PLAYBACK);
assumeTrue(testRan, "Test ddes not allow playback and was ran in 'TestMode.PLAYBACK'");
}

/**
* Returns whether the test should have its network calls recorded during a {@link TestMode#RECORD record} test
* run.
*
* @return Flag indicating whether to record test network calls.
*/
public boolean doNotRecordTest() {
return doNotRecord;
}

/**
* Returns whether the current test was ran.
*
* @return Flag indicating whether the current test was ran.
*/
public boolean wasTestRan() {
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved
return testRan;
}
}
Loading