Testing A Generic Class

Remember in part one we agreed on a really cool statement:

Generally, if you expect a self-written method to do something -ANYTHING- for you, you have to test that expectation!

and we tested an interface with default methods. Now we want to test a generic class, so buckle up, because it won’t as easy as the last one!

Generic classes:

without further ado, let’s dive into the scenario. we have a generic validator service which we supposed that whenever we give it a couple of predicates or filters or conditions or … (you got the idea) it can apply all those conditions against any kind of object that we give it to and indicates the result with any oo these three different flags; Green, Yellow, and Red. easy right?

when it comes to unit testing a generic class we have to be careful to keep this Generalism also in context of testing.

you can think of it as the small bubble next to your Jenkins job with this additional feature that you can tell it the definition of all those three flags and furthermore, the conditions are not always your boring unit/regression/e2e/whatever tests!

No alt text provided for this image

I thought this time giving you an abstraction of what I mean, might be misunderstanding prone, so I decided to give you the whole implementation, while crossing my fingers, that it wouldn’t make you bored, so here is our GenericValidator class.

public class GenericValidator<T> {

    private final List<Predicate<T>> predicates;
    private IValidationConfig validatorConfig= new DefaultConfig();


    public GenericValidator(List<Predicate<T>> p) {
        Objects.requireNonNull(p);
        this.predicates = p;
    }

    public GenericValidator(List<Predicate<T>> p, IValidationConfig validatorConfig) {
        this(p);
        Objects.requireNonNull(validatorConfig);
        this.validatorConfig = validatorConfig;
    }

    public Result validate(T t, IValidationConfig validatorConfig) {
        Objects.requireNonNull(validatorConfig);
        return getResult(t, validatorConfig);
    }

    public Result validate(T t) {
        return getResult(t, validatorConfig);
    }


    private Result getResult(T t, IValidationConfig validatorConfig) {
        Objects.requireNonNull(predicates);
        AtomicInteger i = new AtomicInteger();
        predicates.forEach(p -> {
            if (p.test(t))
                i.getAndIncrement();
        });
        int successPercentage = getSuccessPercentage(i.get());

        if (Integer.compare(successPercentage, validatorConfig.getGreen()) > -1) {
            return Result.GREEN;
        } else if (Integer.compare(successPercentage, validatorConfig.getYellow()) > -1) {
            return Result.YELLOW;
        } else {
            return Result.RED;
        }

    }

    private int getSuccessPercentage(int oks) {
        int preNumbers = predicates.size();
        return oks == preNumbers ? 100 : (oks * 100) / preNumbers;
    }


    public interface IValidationConfig {
        int getGreen();
        int getYellow();
        int getRed();
    }

    public static class DefaultConfig implements IValidationConfig {
        @Override
         public int getGreen() {
            return 85;
        }
        @Override
        public int getYellow() {
            return 80;
        }
        @Override
        public int getRed() {
            return 75;
        }
    }

    public enum Result {
        GREEN,
        YELLOW,
        RED
    }

}

I’m not going to go through each line of code, the fact that ATM this is not our concern. So you can take your time to dig into the class and understand what was my approach.

Now I want to define what should be tested in the case of a generic class!

You just have to keep in your mind that you need to test the general behavior that you expect from your validator, and dictate these behavior to all the concert classes which extends from this generic class!

abstract class GenericValidatorTest<T> {

    private GenericValidator.IValidationConfig ValidationConfig = new GenericValidator.DefaultConfig();

    private final GenericValidator.Result RED = GenericValidator.Result.RED;
    private final GenericValidator.Result YELLOW = GenericValidator.Result.YELLOW;
    private final GenericValidator.Result GREEN = GenericValidator.Result.GREEN;

    private T element;
    private Predicate<T> predicate;


    @Test
    public void testValidate_when_successPercentageIsInGreenRange_shouldReturn_green() {
        List<Predicate<T>> sample = createSample(ValidationConfig, GREEN);
        Assert.assertEquals(GREEN, new GenericValidator<T>(sample).validate(element, ValidationConfig));
    }

    @Test
    public void testValidate_when_successPercentageIsInYellowRange_shouldReturn_yellow() {
        List<Predicate<T>> sample = createSample(ValidationConfig, YELLOW);
        Assert.assertEquals(YELLOW, new GenericValidator<T>(sample).validate(element, ValidationConfig));
    }

    @Test
    public void testValidate_when_successPercentageIsInRedRange_shouldReturn_red() {
        List<Predicate<T>> sample = createSample(ValidationConfig, RED);
        Assert.assertEquals(RED, new GenericValidator<T>(sample).validate(element, ValidationConfig));
    }


    public void whenSamplePredicatesIs(Predicate<T> predicate) {
        this.predicate = predicate;
    }

    public void whenElementIS(T element) {
        this.element = element;
    }

    public void whenValidatorConfigIs(GenericValidator.IValidationConfig config) {
        this.ValidationConfig = config;
    }

    private List<Predicate<T>> createSample(GenericValidator.IValidationConfig severity, GenericValidator.Result expectedResult) {
        Objects.requireNonNull(element, "you should pass an instance of the type. which you want to validate");
        Objects.requireNonNull(predicate);
        if (predicate.negate().test(element))
            throw new IllegalStateException("sample predicate should return true by calling on passed element");

        int trueNumbers;
        if (expectedResult.equals(GenericValidator.Result.YELLOW)) {
            trueNumbers = severity.getYellow() + 1;
        } else if (expectedResult.equals(GenericValidator.Result.RED)) {
            trueNumbers = severity.getRed() - 1;
        } else {
            trueNumbers = severity.getGreen() != 100 ? severity.getGreen() + 1 : 100;
        }
        ArrayList<Predicate<T>> predicates = new ArrayList<>(100);
        IntStream.range(0, trueNumbers).forEach(i -> predicates.add(predicate));
        IntStream.range(0, 100 - trueNumbers).forEach(i -> predicates.add(predicate.negate()));
        return predicates;
    }


}

super important is that you have this class as an Abstract class, which is going to be ignored by Test frameworks, but all the other concert classes which extend this class will see all the three implemented tests as their own, and more importantly they are responsible to keep the general behavior that you defined here, although they can have their tests as well.

let me give you an example :

public class Client {
    private String name;
    private String lastname;
    private long clientNumber;
    private String email;

    public Client(String name, String lastname, long clientNumber, String email) {
        this.name = name;
        this.lastname = lastname;
        this.clientNumber = clientNumber;
        this.email = email;
    }

    public String getName() {
        return name;
    }
    public String getLastname() {
        return lastname;
    }
    public long getClientNumber() {
        return clientNumber;
    }
    public String getEmail() {
        return email;
    }
}

Here is our silly Client Validator:

public class ClientValidator extends GenericValidator<Client> {
    public static Predicate<Client> NAME_VALIDATOR = c -> !c.getName().isEmpty() && !c.getName().contains("#@!");
    public static Predicate<Client> LASTNAME_VALIDATOR = c -> !c.getLastname().isEmpty() && c.getLastname().length() > 10;
    public static Predicate<Client> CLIENT_NUMBER_VALIDATOR = c -> c.getClientNumber() != 0 && c.getClientNumber() > 1000L;
    public static Predicate<Client> CLIENT_EMAIL_VALIDATOR = c -> !c.getEmail().isEmpty() && c.getEmail().contains("@");

    public ClientValidator(List<Predicate<Client>> p) {
        super(p);
    }

}

Our custom validator configuration:

public class StrictValidationConfig implements GenericValidator.IValidationConfig {
    @Override
    public int getGreen() {
        return 50;
    }

    @Override
    public int getYellow() {
        return 40;
    }

    @Override
    public int getRed() {
        return 30;
    }

}

and hopefully, 4 beautiful green ticks should be our result :

No alt text provided for this image

That’s it, if you could follow along with up until here, I have to say, well done! I know that it was not an easy one! but you cracked it!