LifecycleExtension.java

/**
 * Copyright (C) 2022 Christopher J. Stehno
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.github.cjstehno.testthings.junit;

import io.github.cjstehno.testthings.junit.Lifecycle.LifecyclePoint;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.extension.*;
import org.junit.platform.commons.support.ModifierSupport;

import static io.github.cjstehno.testthings.junit.Lifecycle.LifecyclePoint.*;
import static io.github.cjstehno.testthings.util.Reflections.invokeMethod;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN;

/**
 * JUnit 5 extension used to provide a bit more control of the before/after lifecycle methods.
 *
 * Extensions run before the local test instance before/after methods - this can lead to chicken-and-egg issues from
 * time to time. This extension allow you to define methods that will be run before and after, based on where this
 * extension is defined in the list of extensions.
 *
 * For example, if you want to execute a specific before and after method before all of the extensions and before all
 * the other local lifecycle methods, you can configure this extension as the first one in the list, and then the
 * annotated method will execute before all of the other extensions, and then after all of the others at the end.
 */
@Slf4j
public class LifecycleExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback {

    @Override public void beforeAll(final ExtensionContext context) throws Exception {
        invokeMatchingStaticMethods(context.getRequiredTestClass(), BEFORE_ALL);
    }

    /**
     * Finds all the methods annotated with the <code>Lifecycle(BEFORE)</code> annotation and executes them.
     *
     * @param context the current extension context; never {@code null}
     * @throws Exception if there is a problem
     */
    @Override public void beforeEach(final ExtensionContext context) throws Exception {
        invokeMatchingMethods(context.getRequiredTestInstance(), BEFORE_EACH);
    }

    /**
     * Finds all the methods annotated with the <code>Lifecycle(AFTER)</code> annotation, and executes them.
     *
     * @param context the current extension context; never {@code null}
     * @throws Exception if there is a problem
     */
    @Override public void afterEach(final ExtensionContext context) throws Exception {
        invokeMatchingMethods(context.getRequiredTestInstance(), AFTER_EACH);
    }

    @Override public void afterAll(final ExtensionContext context) throws Exception {
        invokeMatchingStaticMethods(context.getRequiredTestClass(), AFTER_ALL);
    }

    private static void invokeMatchingMethods(final Object testInstance, final LifecyclePoint point) {
        findAnnotatedMethods(testInstance.getClass(), Lifecycle.class, TOP_DOWN).stream()
            .filter(m -> m.getAnnotationsByType(Lifecycle.class)[0].value() == point)
            .findAny()
            .ifPresent(m -> invokeMethod(testInstance, m));
    }

    private static void invokeMatchingStaticMethods(final Class<?> testClass, final LifecyclePoint point) {
        findAnnotatedMethods(testClass, Lifecycle.class, TOP_DOWN).stream()
            .filter(ModifierSupport::isStatic)
            .filter(m -> m.getAnnotationsByType(Lifecycle.class)[0].value() == point)
            .findAny()
            .ifPresent(m -> invokeMethod(testClass, m));
    }
}