/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.jupiter.api.extension;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.commons.util.ReflectionUtils;
/**
* Unit tests for extension composability in JUnit Jupiter.
*
* <p>The purpose of these tests is to ensure that a concrete extension
* (a.k.a., the kitchen sink extension) is able to implement all extension
* APIs supported by JUnit Jupiter without any naming conflicts or
* ambiguities with regard to method names or method signatures.
*
* @since 5.0
* @see KitchenSinkExtension
*/
class ExtensionComposabilityTests {
@Test
void ensureJupiterExtensionApisAreComposable() {
// 1) Find all existing top-level Extension APIs
List<Class<?>> extensionApis = ReflectionUtils.findAllClassesInPackage(Extension.class.getPackage().getName(),
this::isExtensionApi, name -> true);
// 2) Determine which methods we expect the kitchen sink to implement...
// @formatter:off
List<Method> expectedMethods = extensionApis.stream()
.map(Class::getDeclaredMethods)
.flatMap(Arrays::stream)
.collect(toList());
List<String> expectedMethodSignatures = expectedMethods.stream()
.map(this::methodSignature)
.sorted()
.collect(toList());
List<String> expectedMethodNames = expectedMethods.stream()
.map(Method::getName)
.sorted()
.collect(toList());
// @formatter:on
// 3) Dynamically implement all Extension APIs
Object dynamicKitchenSinkExtension = Proxy.newProxyInstance(getClass().getClassLoader(),
extensionApis.toArray(new Class<?>[extensionApis.size()]), (proxy, method, args) -> null);
// 4) Determine what ended up in the kitchen sink...
// @formatter:off
List<Method> actualMethods = Arrays.stream(dynamicKitchenSinkExtension.getClass().getDeclaredMethods())
.collect(toList());
List<String> actualMethodSignatures = actualMethods.stream()
.map(this::methodSignature)
.distinct()
.sorted()
.collect(toList());
List<String> actualMethodNames = actualMethods.stream()
.map(Method::getName)
.distinct()
.sorted()
.collect(toList());
// @formatter:on
// 5) Remove methods from java.lang.Object
actualMethodSignatures.remove("equals(Object)");
actualMethodSignatures.remove("hashCode()");
actualMethodSignatures.remove("toString()");
actualMethodNames.remove("equals");
actualMethodNames.remove("hashCode");
actualMethodNames.remove("toString");
// 6) Verify our expectations
// @formatter:off
assertAll(
() -> assertThat(actualMethodSignatures).isEqualTo(expectedMethodSignatures),
() -> assertThat(actualMethodNames).isEqualTo(expectedMethodNames)
);
// @formatter:on
}
private boolean isExtensionApi(Class<?> candidate) {
return candidate.isInterface() && (candidate != Extension.class) && Extension.class.isAssignableFrom(candidate);
}
private String methodSignature(Method method) {
return String.format("%s(%s)", method.getName(),
ClassUtils.nullSafeToString(Class::getSimpleName, method.getParameterTypes()));
}
}