Build Status MIT License Javadocs

The Problem

How to test refactored code that originally had no tests, unclear specification, no owner, and just begged to end it's misery ?

Solution

Let the the users do it for you! All you need are few steps.

Identify your enemy

public class RentalDiscountService {

    // help needed!! but to be honest, it could've been worse...
    public Double calculateDiscountPercent(Integer movieType, Integer rentalPeriod) {
        if (movieType == 1 || (movieType != 3 &&  rentalPeriod == -1)) {
            return 10.0;
        } else if (movieType == 2 && rentalPeriod == 1) {
            return 40.0;
        } else if (movieType == 4 && rentalPeriod > 10) {
            return 5.0;
        } else {
            return 0.0;
        }
    }
}

Add maven dependency

<dependency>
    <groupId>com.github.darwin-evolution</groupId>
    <artifactId>darwin</artifactId>
    <version>0.9.1</version>
</dependency>

Create art not code

public class RentalDiscountCalculator {

    private static final BigDecimal NO_DISCOUNT = BigDecimal.ZERO;
    private static final BigDecimal WEEKEND_DISCOUNT = BigDecimal.TEN;
    private static final BigDecimal ROMANTIC_EVENING_DISCOUNT = new BigDecimal(40.0);
    private static final BigDecimal FAMILY_WEEK = new BigDecimal(5.0);

    //piece of art, not code... (please don't judge)
    public BigDecimal calculateDiscount(MovieType movieType, RentalPeriod rentalPeriod) {
        if (movieType.equals(MovieType.COMEDY)
                || (!movieType.equals(MovieType.HORROR) && rentalPeriod.isWeekend())) {
            return WEEKEND_DISCOUNT;
        }

        if (movieType.equals(MovieType.ROMANCE) && rentalPeriod.isOverNight()) {
            return ROMANTIC_EVENING_DISCOUNT;
        }

        if (movieType.equals(MovieType.FAMILY) && rentalPeriod.isLongRental())  {
            return FAMILY_WEEK;
        }

        return NO_DISCOUNT;
    }
}

Write evolution

We encapsulate both implementations in single evolution, in order to test and measure them. Both implementations are executed in sequence, starting with legacy one.

import com.github.darwinevolution.darwin.Evolution;
import com.github.darwinevolution.darwin.execution.harness.EvolvedExecutionHarness;
import com.github.darwinevolution.darwin.execution.harness.ProtoplastExecutionHarness;

public class RentalDiscountService {

    private RentalDiscountCalculator rentalDiscountCalculator;

    // we rule don't we?
    public Double calculateDiscountPercent(final Integer movieType, final Integer rentalPeriod) throws Exception {
        return Evolution.<Double>of("calculateDiscountPercent")
                .from(new ProtoplastExecutionHarness<Double>() {
                    @Override
                    public Double execute() throws Exception {
                        arguments(movieType, rentalPeriod); // arguments for logging purposes

                        // run our legacy implementation
                        return legacyCalculateDiscountPercent(movieType, rentalPeriod); 
                    }
                }).to(new EvolvedExecutionHarness<Double>() {
                    @Override
                    public Double execute() throws Exception {
                        arguments(movieType, rentalPeriod); // arguments for logging purposes

                        // unleash the beauty!
                        return rentalDiscountCalculator.calculateDiscount(MovieTypeFactory.from(movieType), 
                            RentalPeriod.from(rentalPeriod)).doubleValue();
                    }
                }).evolve();
    }

    // we need to extract it and contain it, before it spreads around all our system...
    private Double legacyCalculateDiscountPercent(Integer movieType, Integer rentalPeriod) {
        if (movieType == 1 || (movieType != 3 && rentalPeriod == -1)) {
            return 10.0;
        } else if (movieType == 2 && rentalPeriod == 1) {
            return 40.0;
        } else if (movieType == 4 && rentalPeriod > 10) {
            return 5.0;
        } else {
            return 0.0;
        }
    }
}

Configure logger

Results of our evolution executions will be logged here...

<Loggers>
    <Logger name="darwin.evolution" level="trace" additivity="false">
        ...
    </Logger>
</Loggers>

Build it, ship it

Build your app and install in any environment (dev, qa, prod).

Analyze produced logs

When someone executes our RentalDiscountService.calculateDiscountPercent method, our logs will contain entries like this (more info)

"calculateDiscountPercent"|"PROTOPLAST"|"1463682597919"|"OK"||"249896"||||"171597"|||

You noticed big juicy OK? It means values returned by both implementations were equal, so we've just tested our new code without moving a finger. It's nice to have users...

Remove old implementation

After incubation period, may it be a week or a month, when you see in logs that every call behaved exactly the same as the old one (or even better) you can safely delete all traces of old implementation alongside evolution associated with it. And now you are one step closer to perfection!

Documentation

Visit our project page.

Authors and Contributors

Damian Kolasa (@fatfredyy)

Support or Contact

Having trouble with Darwin? Check out our groups.