Skip to content

Instantly share code, notes, and snippets.

@matthias-dirickx
Created April 10, 2020 15:45
Show Gist options
  • Save matthias-dirickx/77beee2d2da758386dbe7f6a012f046c to your computer and use it in GitHub Desktop.
Save matthias-dirickx/77beee2d2da758386dbe7f6a012f046c to your computer and use it in GitHub Desktop.
Guard test context over classes. Designed to be thread-safe, but not guarnateed to be threadsafe. In response to the need to having a flexible test context without adding DI (which might often be the better option if I have to believe the google search results to this issue).
package be.mdi.test.utils;
import java.util.HashMap;
public class TestContext {
/*
* TestContext Class Methods
*
*/
/**
* Variable holding the map of maps for the stored variables.
*/
private ScenarioVariableHolder scenarioVariableHolder;
/**
* <p>Private constructor.</P>
* <p>Used by the singleton implementation only.</p>
*
* <p>The sole responsibility is to instantiate the ScenarioVariableHolder object into the created singleton.</p>
*/
private TestContext() {
scenarioVariableHolder = new ScenarioVariableHolder();
}
/**
* <p>Implemented for testing purposes mainly.</p>
* <p>Reset is a blunt method that initializes the ScenarioVariableHolder again.</p>
* <p>In real-life cases this may be a bad idea because it may be reset by one tread while another thread still depends on it.</p>
*/
private void reset() {
scenarioVariableHolder = new ScenarioVariableHolder();
}
/*
* Singleton access methods.
*
* Bill Pugh Singleton should be thread-safe for parallel access in multicore environments.
*
*/
/**
* Private static inner class that holds the singleton.
*
* @author Matthias.Dirickx
*/
private static class TestContextSingleton {
private static final TestContext instance = new TestContext();
}
/**
* <p>Add a variable to the collection.</p>
* <p>The first identifier is for the Scenario or Test Case.</p>
* <p>The two other variables are for the variable identifier (the name),
* and the variable value (the data).</p>
* <p>The {@link Scenario} object is called with the {@link Scenario#getId()} method
* to get the Id. The id is stored in the map rather than the scenario object
* because the id is thought to be immutable during execution
* (path + line number of scenario name). The scenario will be updated
* and will likely not be equal ( == ) to the stored scenario object.</p>
*
*
* @param scenario <p>{@link Scenario} - must be taken from the step defs to pass on.
* @param name <p>{@link String} - must be unique. Duplicate name for a scenario will throw an IllegalArgumentException.</p>
* @param data <p>{@link Object} - any object. Note that this means that you can pass Integer, but not the primitive int as that is not an object.</p>
*/
public static void add(String identifier, String name, Object data) {
TestContextSingleton.instance.scenarioVariableHolder.add(identifier, name, data);
}
/**
* <p>Get a variable with name {@code name} from Scenario {@code scenario}
* as type {@code clazz}.</p>
* <p>The generic implemntation allows to store random objects and to retreive them this way.
* This does put the responsibility of knowing which type it is with the client.</p>
* <p>This is not outrageous as the code retrieving the variable should know what it's going to get.</p>
*
* @param clazz <p>{@link Class<T>} - A class object with the requested return type.</p>
* @param scenario <p>{@link Scenario} - The Scenario you request the variable for. </p>
* @param name <p>{@link String} - The name of the variable.</p>
* @return T <p>Returns an object cast to Class T. This allows for generically returning variables as long a the client knows what he's doing.</p>
*/
public static <T> T get(Class<T> clazz, String identifier, String name) {
return (T)TestContextSingleton.instance.scenarioVariableHolder.get(clazz, identifier, name);
}
/**
* Returns the number of scenario's stored.
*
* @return {@link int}
*/
public static int getScenarioCount() {
return TestContextSingleton.instance.scenarioVariableHolder.getScenarioCount();
}
/**
* <p>Returns the number of variables stored for a given scenario.</p>
*
* @param scenario <p>{@link Scenario} - The scenario to get the count for.</p>
* @return {@link int}
*/
public static int getVariableCount(String identifier) {
return TestContextSingleton.instance.scenarioVariableHolder.getVarCount(identifier);
}
/**
* <p>Remove all variables for a given scenario as well as the scenario itself form the map.</p>
* @param scenario <p>{@link Scenario} - Scenario to delete.</p>
*/
public static void flush(String identifier) {
TestContextSingleton.instance.scenarioVariableHolder.flush(identifier);
}
/**
* <p>Implemented for testing purposes.</p>
* <p>This resets the singleton's variable used to store the data.</p>
* <p>Unless it makes sense (like in testing this class where you need
* to reset the instance between cases) this should not be used</p>
*/
public static void resetSingleton() {
TestContextSingleton.instance.reset();
}
/*
* Structural Inner Classes:
* - ScenarioVariableHolder: a Map of Maps that links a scenario object to a variables.
* - SceanarioVariables: a Map of String to Object with the String representing the name.
*
* The name must be unique within the map of the scenario.
* The update with the put is checked with a if same value expression returning an error.
*
*/
/**
* <p>Store for a defined map of maps.</p>
*
* <p>The storage is a map of String (as key) to {@link ScenarioVariables},
* holding the key/value pairs for the actual variables for the scenario.</p>
*
* @author Matthias.Dirickx
*
*/
private class ScenarioVariableHolder {
private HashMap<String, ScenarioVariables> scenarioVarCollection;
ScenarioVariableHolder() {
scenarioVarCollection = new HashMap<String, ScenarioVariables>();
}
private void add(String id, String name, Object data) {
if(scenarioVarCollection.containsKey(id)) {
scenarioVarCollection.get(id).add(name, data);
} else {
scenarioVarCollection.put(id, new ScenarioVariables().add(name, data));
}
}
private <T> T get(Class<T> clazz, String id, String name) {
return (T)scenarioVarCollection.get(id).get(clazz, name);
}
private void flush(String id) {
if(scenarioVarCollection.containsKey(id)) {
scenarioVarCollection.remove(id);
}
}
/*
* Utility functions for testing purposes.
*/
private int getScenarioCount() {
return scenarioVarCollection.size();
}
private int getVarCount(String id) {
return scenarioVarCollection.get(id).getCount();
}
}
/**
* A defined map of String to object to hold a list of uniquely defined variables.
* It is the client responsibility to manage unique names per scenario.
* @author Matthias.Dirickx
*
*/
private class ScenarioVariables {
private HashMap<String, Object> vars;
ScenarioVariables() {
vars = new HashMap<String, Object>();
}
public ScenarioVariables add(String name, Object data) throws IllegalArgumentException {
if(vars.containsKey(name)) {
throw new IllegalArgumentException(
String
.format(
"The map for the scenario already contains the key '{}'",
name)
);
}
vars.put(name, data);
return this;
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> clazz, String name) {
return (T)vars.get(name);
}
/*
* Utility function for testing purposes.
*/
public int getCount() {
return vars.size();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment