All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xwiki.test.mockito.MockitoComponentMockingRule Maven / Gradle / Ivy

There is a newer version: 8.2-milestone-1
Show newest version
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.test.mockito;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Provider;

import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.xwiki.component.annotation.ComponentAnnotationLoader;
import org.xwiki.component.annotation.ComponentDescriptorFactory;
import org.xwiki.component.annotation.Role;
import org.xwiki.component.descriptor.ComponentDependency;
import org.xwiki.component.descriptor.ComponentDescriptor;
import org.xwiki.component.descriptor.DefaultComponentDescriptor;
import org.xwiki.component.internal.RoleHint;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.util.ReflectionUtils;

/**
 * Unit tests for Components should extend this class instead of using {@link MockitoComponentManagerRule} or
 * {@link org.xwiki.test.ComponentManagerRule} which should only be used for integration tests.
 * 

* To use this class, define a JUnit {@code @Rule} and pass the component implementation class that you wish to have * mocked for you. Then in your test code, do a lookup of your component under test and you'll get a component instance * which has all its injected dependencies mocked automatically. *

* For example: *

{@code
 * public class MyComponentTest
 * {
 *     @Rule
 *     public final MockitoComponentMockingRule<MyComponent> mocker =
 *         new MockitoComponentMockingRule(MyImplementation.class);
 * 
 *     @Test
 *     public void someTest() throws Exception
 *     {
 *         MyComponent myComponent = mocker.getComponentUnderTest();
 *         ...
 *     }
 * ...
 * }
 * }
* * Note that by default there are no component registered against the component manager except those mocked * automatically by the Rule (except for the MockitoComponentMockingRule itself, which means that if your component * under test is injected a default ComponentManager, it'll be the MockitoComponentMockingRule which will get injected. * See more below). This has 2 advantages: *
    *
  • This is the spirit of this Rule since it's for unit testing and this testing your component in isolation from * the rest
  • *
  • It makes the tests up to 10 times faster
  • *
* If you really need to register some components, use the {@link org.xwiki.test.annotation.ComponentList} annotation * and if you really really need to register all components (it takes time) then use * {@link org.xwiki.test.annotation.AllComponents}. *

* In addition, you can perform some action before any component is registered in the Component Manager by having one or * several methods annotated with {@link org.xwiki.test.annotation.BeforeComponent}. Similarly, you can perform an * action after all components have been registered in the Component Manager by having one or several methods annotated * with {@link org.xwiki.test.annotation.AfterComponent}. *

* This can be useful (for example) in the case you wish to register a mock ComponentManager in your component under * test. You would write: *

{@code
 * @Rule
 * public final MockitoComponentManagerRule mocker = new MockitoComponentManagerRule();
 * 
 * @AfterComponent
 * public void overrideComponents() throws Exception
 * {
 *     this.mocker.registerMockComponent(ComponentManager.class);
 * }
 * }
* * @param the component role type, used to provide a typed instance when calling {@link #getComponentUnderTest()} * @version $Id: ef448d1abe03e8a1cf3ec03e3bc57c0365663e63 $ * @since 4.3.1 */ public class MockitoComponentMockingRule extends MockitoComponentManagerRule { /** * Used to discover and register components using annotations. */ private ComponentAnnotationLoader loader = new ComponentAnnotationLoader(); /** * Used to create Component Descriptors based on annotations. */ private ComponentDescriptorFactory factory = new ComponentDescriptorFactory(); /** * The mocked logger if any. */ private Logger mockLogger; /** * The Role Type and Hint of the mocked component. */ private RoleHint mockedComponentHint; /** * The class of the component implementation to mock. */ private Class componentImplementationClass; /** * The list of component Roles that shouldn't be mocked. */ private List> excludedComponentRoleDependencies = new ArrayList>(); /** * The role Type if the component implementation implements several roles. */ private Type componentRoleType; /** * The role Hint if the component implementation implements several roles. */ private String componentRoleHint; /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked */ public MockitoComponentMockingRule(Class componentImplementationClass) { this.componentImplementationClass = componentImplementationClass; } /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked * @param excludedComponentRoleDependencies list of component dependency role classes that we don't want mocked */ public MockitoComponentMockingRule(Class componentImplementationClass, List> excludedComponentRoleDependencies) { this(componentImplementationClass); this.excludedComponentRoleDependencies.addAll(excludedComponentRoleDependencies); } /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked * @param componentRoleType the role type of the component implementation (when it has several), for disambiguation * @param componentRoleHint the role hint of the component implementation (when it has several), for disambiguation * @param excludedComponentRoleDependencies list of component dependency role classes that we don't want mocked */ public MockitoComponentMockingRule(Class componentImplementationClass, Type componentRoleType, String componentRoleHint, List> excludedComponentRoleDependencies) { this(componentImplementationClass, excludedComponentRoleDependencies); this.componentRoleType = componentRoleType; this.componentRoleHint = componentRoleHint; } /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked * @param componentRoleType the role type of the component implementation (when it has several), for disambiguation * @param excludedComponentRoleDependencies list of component dependency role classes that we don't want mocked */ public MockitoComponentMockingRule(Class componentImplementationClass, Type componentRoleType, List> excludedComponentRoleDependencies) { this(componentImplementationClass, componentRoleType, null, excludedComponentRoleDependencies); } /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked * @param componentRoleType the role type of the component implementation (when it has several), for disambiguation * @param componentRoleHint the role hint of the component implementation (when it has several), for disambiguation */ public MockitoComponentMockingRule(Class componentImplementationClass, Type componentRoleType, String componentRoleHint) { this(componentImplementationClass); this.componentRoleType = componentRoleType; this.componentRoleHint = componentRoleHint; } /** * @param componentImplementationClass the component implementation for which we wish to have its injection mocked * @param componentRoleType the role type of the component implementation (when it has several), for disambiguation */ public MockitoComponentMockingRule(Class componentImplementationClass, Type componentRoleType) { this(componentImplementationClass, componentRoleType, (String) null); } @Override public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { return new Statement() { @Override public void evaluate() throws Throwable { before(base, method, target); try { base.evaluate(); } finally { after(base, method, target); } } }; } /** * Called before the test. * * @param base The {@link Statement} to be modified * @param method The method to be run * @param target The object on with the method will be run. * @throws Throwable if anything goes wrong * @since 5.1M1 */ @Override protected void before(final Statement base, final FrameworkMethod method, final Object target) throws Throwable { super.before(base, method, target); mockComponent(target); } /** * Mock the injected components for the specified component implementation. * * @param testInstance the test instance * @throws Exception in case of an error while mocking */ private void mockComponent(final Object testInstance) throws Exception { // Handle component fields for (ComponentDescriptor descriptor : this.factory.createComponentDescriptors( this.componentImplementationClass, findComponentRoleType())) { // Only use the descriptor for the specified hint if ((this.componentRoleHint != null && this.componentRoleHint.equals(descriptor.getRoleHint())) || this.componentRoleHint == null) { registerMockDependencies(descriptor); registerComponent(descriptor); // Save the mocked component information so that the test can get an instance of this component // easily by calling getComponentUnderTest(...) this.mockedComponentHint = new RoleHint(descriptor.getRoleType(), descriptor.getRoleHint()); break; } } } /** * Overrides EmbeddableComponentManager in order to mock Loggers since they're handled specially and are not * components. * * @param instanceClass the injected class * @return the logger */ @Override protected Object createLogger(Class instanceClass) { Object logger; if (!this.excludedComponentRoleDependencies.contains(Logger.class) && this.componentImplementationClass == instanceClass) { logger = mock(Logger.class, instanceClass.getName()); this.mockLogger = (Logger) logger; } else { logger = super.createLogger(instanceClass); } return logger; } /** * Create mocks of injected dependencies and registers them against the Component Manager. * * @param descriptor the descriptor of the component under test * @throws Exception if an error happened during registration */ private void registerMockDependencies(ComponentDescriptor descriptor) throws Exception { Collection> dependencyDescriptors = descriptor.getComponentDependencies(); for (ComponentDependency dependencyDescriptor : dependencyDescriptors) { Class roleTypeClass = ReflectionUtils.getTypeClass(dependencyDescriptor.getRoleType()); // Only register a mock if it isn't: // - Already registered // - An explicit exception specified by the user // - A logger // - A collection of components, we want to keep them as Java collections. Those collections are later // filled by the component manager with available components. Developers can register mocked components // in an override of #setupDependencies(). // TODO: Handle multiple roles/hints. if (!this.excludedComponentRoleDependencies.contains(roleTypeClass) && Logger.class != roleTypeClass && !roleTypeClass.isAssignableFrom(List.class) && !roleTypeClass.isAssignableFrom(Map.class) && !hasComponent(dependencyDescriptor.getRoleType(), dependencyDescriptor.getRoleHint())) { DefaultComponentDescriptor cd = new DefaultComponentDescriptor<>(); cd.setRoleType(dependencyDescriptor.getRoleType()); cd.setRoleHint(dependencyDescriptor.getRoleHint()); Object dependencyMock = mock(roleTypeClass, dependencyDescriptor.getName()); if (Provider.class == roleTypeClass) { Type providedType = ReflectionUtils.getLastTypeGenericArgument(dependencyDescriptor.getRoleType()); Class providedClass = ReflectionUtils.getTypeClass(providedType); // If the target is registered or if a list or a map are asked don't mock anything if (hasComponent(providedType, dependencyDescriptor.getRoleHint()) || providedClass.isAssignableFrom(List.class) || providedClass.isAssignableFrom(Map.class)) { continue; } if (providedClass.getAnnotation(Role.class) != null) { // If the dependency is a Provider for a @Role mock the @Role instead of the Provider cd.setRoleType(providedType); dependencyMock = mock(providedClass, dependencyDescriptor.getName()); } else { // If the dependency is a Provider not targeting a @Role register a mock Provider which provide // a mock Provider provider = (Provider) dependencyMock; when(provider.get()).thenReturn(mock(providedClass, providedType.toString())); } } registerComponent(cd, dependencyMock); } } } /** * @return the Component role type extracted from the the component implementation class */ private Type findComponentRoleType() { Type type; Set componentRoleTypes = this.loader.findComponentRoleTypes(this.componentImplementationClass); if (this.componentRoleType != null) { if (!componentRoleTypes.contains(this.componentRoleType)) { throw new RuntimeException("Specified Component Role not found in component"); } else { type = this.componentRoleType; } } else { if (componentRoleTypes.isEmpty()) { throw new RuntimeException(String.format("Couldn't find roles for component [%s]", this.componentRoleType)); } else if (componentRoleTypes.size() > 1) { throw new RuntimeException("Components with several roles must explicitly specify which role to use."); } else { type = componentRoleTypes.iterator().next(); } } return type; } /** * @return the component which is having its injections being mocked by the {@link MockitoComponentMockingRule} rule * @throws ComponentLookupException if the component under test has not been properly registered */ public T getComponentUnderTest() throws ComponentLookupException { return getInstance(this.mockedComponentHint.getRoleType(), this.mockedComponentHint.getHint()); } /** * @return the mocked Logger if the Component under Test has requested an Injection of a Logger or null otherwise */ public Logger getMockedLogger() { // Note: the test class must get the mocked component instance before calling this method since this is this // action that injects the mock loggers... Note that we cannot call the mocked component here since the // component can be per-lookup and we would get a different instance... return this.mockLogger; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy