001/*
002 * Copyright © 2018. Sir Wellington.
003 * Licensed under the Apache License, Version 2.0 (the "License");
004 * you may not use this file except in compliance with the License.
005 *
006 * You may obtain a copy of the License at
007 *     http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software
010 * distributed under the License is distributed on an "AS IS" BASIS,
011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 * See the License for the specific language governing permissions and
013 * limitations under the License.
014 */
015package tech.sirwellington.alchemy.test.junit.runners;
016
017import org.junit.runner.notification.RunNotifier;
018import org.junit.runners.BlockJUnit4ClassRunner;
019import org.junit.runners.model.*;
020import org.mockito.*;
021import org.mockito.internal.junit.UnnecessaryStubbingsReporter;
022import org.mockito.internal.runners.util.FailureDetector;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026/**
027 * Alchemy Test Runner Features:
028 * <p>
029 * <ul>
030 * <li> Initializes Mockito {@linkplain Mock @Mocks}
031 * <li> Prints out the testName to the console using {@code System.out.println()}
032 * <li> Can repeat your tests using the {@linkplain  Repeat @Repeat} annotation
033 * <li> Initialize generated Data Using {@link GenerateString}, {@link GenerateInteger}, etc..
034 * <p>
035 * </ul>
036 *
037 * @author SirWellington
038 * @see <a href="https://github.com/SirWellington/alchemy-test">https://github.com/SirWellington/alchemy-test</a>
039 */
040public class AlchemyTestRunner extends BlockJUnit4ClassRunner
041{
042
043    private final static Logger LOG = LoggerFactory.getLogger(AlchemyTestRunner.class);
044
045    //If false, we won't initialize Mockito's mocks
046    private boolean shouldInitMocks = true;
047
048    public AlchemyTestRunner(Class<?> klass) throws InitializationError
049    {
050        super(klass);
051        shouldInitMocks = shouldInitMockitoMocks();
052    }
053
054    @Override
055    protected Statement withBefores(FrameworkMethod method, final Object target, Statement statement)
056    {
057        final Statement superStatement = super.withBefores(method, target, statement);
058
059        return new Statement()
060        {
061            @Override
062            public void evaluate() throws Throwable
063            {
064                if (shouldInitMocks)
065                {
066                    MockitoAnnotations.initMocks(target);
067                }
068
069                TestClassInjectors.populateGeneratedFields(getTestClass(), target);
070
071                superStatement.evaluate();
072            }
073        };
074    }
075
076    @Override
077    protected Statement methodBlock(final FrameworkMethod method)
078    {
079        int timesToRun = determineTimesToRun(method);
080
081        Provider<Statement> statementFactory = new Provider<Statement>()
082        {
083            @Override
084            public Statement get()
085            {
086                return AlchemyTestRunner.super.methodBlock(method);
087            }
088        };
089
090        return new RepeatStatement(timesToRun, statementFactory, method);
091    }
092
093    @Override
094    public void run(RunNotifier notifier)
095    {
096        if (shouldInitMocks)
097        {
098            //Allow Mockito to do its verification
099            UnnecessaryStubbingsReporter reporter = new UnnecessaryStubbingsReporter();
100            FailureDetector listener = new FailureDetector();
101
102            Mockito.framework().addListener(reporter);
103
104            try
105            {
106                notifier.addListener(listener);
107            }
108            finally
109            {
110                Mockito.framework().removeListener(reporter);
111            }
112        }
113
114        super.run(notifier);
115    }
116
117    private int determineTimesToRun(FrameworkMethod method)
118    {
119
120        DontRepeat dontRepeatAnnotation = method.getAnnotation(DontRepeat.class);
121
122        if (dontRepeatAnnotation != null)
123        {
124            return 1;
125        }
126
127        Repeat repeatAnnotationOnMethod = method.getAnnotation(Repeat.class);
128
129        if (repeatAnnotationOnMethod != null)
130        {
131            int value = repeatAnnotationOnMethod.value();
132
133            if (value <= 0)
134            {
135                LOG.error(method.getName() + " annotated with a negative @Times. Defaulting to 1");
136                value = 1;
137            }
138
139            return value;
140        }
141
142        Repeat repeatAnnotationOnClass = getTestClass().getAnnotation(Repeat.class);
143
144        if (repeatAnnotationOnClass != null)
145        {
146            int value = repeatAnnotationOnClass.value();
147
148            if (value <= 0)
149            {
150                LOG.error(getTestClass().getName() + " annotated with a negative @Times. Defaulting to 1");
151                value = 1;
152            }
153
154            return value;
155        }
156
157        return 1;
158    }
159
160    private boolean shouldInitMockitoMocks()
161    {
162        TestClass testClass = this.getTestClass();
163        InitMocks initMocks = testClass.getAnnotation(InitMocks.class);
164
165        if (initMocks != null)
166        {
167            return initMocks.value();
168        }
169
170        return true;
171    }
172
173}