Loading...

Resource management in tests

 I guess that in testing projects resource management can be also quite important and valid topic. Resources which goes into my mind in this case are for example users used for tests purposes. Usually there are no problems with creating users of tested application in/by tests. It changes when you use external (paid) service for managing users or access rights. In such case creating hundreds of users per day can be quite expensive. Solution for that can be reuse users created previously and distribute them to your tests. In this article I will explain how to do that with parallel tests execution.

For start we need a class representation of our user. The object of this class will be reflecting user data created before in AWS Cognito service. Simplified version of this class can looks like shown below:

package com.testcraftsmanship.resourcesmanagement.resources.items;

import java.util.Objects;

public class CognitoUser {
private String firstName;
private String lastName;
private String userName;
private String email;
private String password;

public CognitoUser(String firstName, String lastName) {
	this.firstName = firstName;
	this.lastName = lastName;
	this.userName = firstName.toLowerCase() + "." + lastName.toLowerCase();
	//logic for setting other fields
	}
}


The next what we need is the heart of our solution - class which will be reserving and releasing previously releasing users.

package com.testcraftsmanship.resourcesmanagement.resources;

import com.testcraftsmanship.resourcesmanagement.exceptions.ResourceNotAvailableException;
import com.testcraftsmanship.resourcesmanagement.resources.items.CognitoUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

public class ResourcesManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(ResourcesManager.class);
    private Set<CognitoUser> availableCognitoUsers;
    private static ResourcesManager instance;
    private static final int USER_AVAILABILITY_TIMEOUT_IN_SECONDS = 30;
    private static final int USER_AVAILABILITY_POOLING_IN_SECONDS = 1;
    private static final int MILLISECONDS_IN_SECOND = 1000;
    public static final Set<CognitoUser> COGNITO_USERS = Set.of(
            new CognitoUser("Khouthoum", "Dragondigger"),
            new CognitoUser("Grusgrul", "Duskshield"),
            new CognitoUser("Kheznaelynn", "Heavyfury"),
            new CognitoUser("Falael", "Wranvaris"),
            new CognitoUser("Kellam", "Ergwyn")
    );

    private ResourcesManager() {
        this.availableCognitoUsers = new HashSet<>(COGNITO_USERS);
    }

    public static ResourcesManager getResourcesManager() {
        if (instance == null) {
            instance = new ResourcesManager();
        }
        return instance;
    }

    public CognitoUser reserveUser() {
        final long timeout = new Date().getTime() + USER_AVAILABILITY_TIMEOUT_IN_SECONDS * MILLISECONDS_IN_SECOND;
        while (timeout > new Date().getTime()) {
            Optional<CognitoUser> availableUser = getFirstAvailableUser();
            if (availableUser.isPresent()) {
                return availableUser.get();
            }
            wait(USER_AVAILABILITY_POOLING_IN_SECONDS);
        }
        throw new ResourceNotAvailableException("There is no available user to get");
    }

    public synchronized void releaseUsers(Collection<CognitoUser> users) {
        for (CognitoUser user : users) {
            if (COGNITO_USERS.contains(user)) {
                availableCognitoUsers.add(user);
            } else {
                LOGGER.warn("Unable to release user {}", user);
            }
            LOGGER.info("Released user: {}", user);
        }
    }

    private synchronized Optional<CognitoUser> getFirstAvailableUser() {
        if (availableCognitoUsers.isEmpty()) {
            return Optional.empty();
        }
        CognitoUser user = availableCognitoUsers.iterator().next();
        availableCognitoUsers.remove(user);
        LOGGER.info("Reserved user: {}", user);
        return Optional.of(user);
    }

    private void wait(int seconds) {
        try {
            Thread.sleep(seconds * MILLISECONDS_IN_SECOND);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


In the class above we have set COGNITO_USERS which contains all users we have in cognito and can be used by our tests. We will be reserving user from this list before each tests and releasing it when the test is finished. As you can see this is singleton class to ensure that only one object is responsible for managing our users. Method reserveUser() is responsible for extracting first available user from the Set. When no users are available then method waits maximum 30 seconds for releasing the user. The method uses synchronized method getFirstAvailableUser(). This one just extract the first available user from set and removes it. As it is synchronized we ensure that method responsible for releasing the user (adding to set) will not be run in parallel.
Now lets create interface which will be responsible for creating the test data before each test.

package com.testcraftsmanship.resourcesmanagement.resources;

import com.testcraftsmanship.resourcesmanagement.resources.items.CognitoUser;

public interface TestDataCreator {

    default CognitoUser createTestPreconditions() {
        CognitoUser user = setUpPredefinedUser();
        //Other logic which set ups test data with use of user
        return user;
    }

    CognitoUser setUpPredefinedUser();
}


The createTestPreconditions() will be reserving the user and creating all other staff to make our test working. So e.g. it can connect to test application database and set desired relations to the reserved user and put other required items to database. This interface will be implemented by our BaseTest class which will be extended by all our test classes.

package com.testcraftsmanship.resourcesmanagement;

import com.testcraftsmanship.resourcesmanagement.resources.ResourcesManager;
import com.testcraftsmanship.resourcesmanagement.resources.TestDataCreator;
import com.testcraftsmanship.resourcesmanagement.resources.items.CognitoUser;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestInfo;

import java.util.*;

public class BaseTest implements TestDataCreator {
    private static final String REMAINING_USERS_KEY = "testNameNotAccessible";
    private static final String CLASS_NAME_FOLLOWING_TEST_NAME_IN_STACK_TRACE = "jdk.internal.reflect.NativeMethodAccessorImpl";
    private static Map<String, List<CognitoUser>> usersUsedInTestCase = new HashMap<>();

    @AfterEach
    public void cleanUp(TestInfo testInfo){
        if (testInfo.getTestMethod().isPresent()) {
            final String testMethodName = testInfo.getTestMethod().get().getName();
            cleanUpUsersCreatedWhileTestCase(testMethodName);
        }
    }

    @AfterAll
    public static void cleanUp() {
        cleanUpUsersCreatedWhileTestCase(REMAINING_USERS_KEY);
    }

    public CognitoUser setUpPredefinedUser() {
        CognitoUser user = ResourcesManager.getResourcesManager().reserveUser();
        final String testMethodName = extractTestMethodName();
        List<CognitoUser> usersPerTest;
        if (usersUsedInTestCase.containsKey(testMethodName)) {
            usersPerTest = usersUsedInTestCase.get(testMethodName);
        } else {
            usersPerTest = new ArrayList<>();
        }
        usersPerTest.add(user);
        usersUsedInTestCase.put(testMethodName, usersPerTest);
        return user;
    }

    private static void cleanUpUsersCreatedWhileTestCase(String testMethodName) {
        if (usersUsedInTestCase.containsKey(testMethodName)) {
            ResourcesManager.getResourcesManager().releaseUsers(usersUsedInTestCase.get(testMethodName));
        }
    }

    private String extractTestMethodName() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        for (int i = 0; i < stackTraceElements.length; i++) {
            if (stackTraceElements[i].getClassName().equals(CLASS_NAME_FOLLOWING_TEST_NAME_IN_STACK_TRACE)) {
                return stackTraceElements[i - 1].getMethodName();
            }
        }
        return REMAINING_USERS_KEY;
    }
}


There are two most important methods in the class above. Method setUpPredefinedUser() fills in the map with users reserved by the test case method. Whenever it is called it extracts the test method in which it was called and put the name as a key to the map. As a value it puts the list of users created in desired test. The method cleanUpUsersCreatedWhileTestCase() is responsible for releasing users used by the test case method which just has finished. After performing that action users are once again available for reserving by other tests.

Draft project which uses that code is available here.