google-nomulus/javatests/google/registry/testing/InjectRule.java
mcilwain aa2f283f7c Convert entire project to strict lexicographical import sort ordering
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=127234970
2016-07-13 15:59:53 -04:00

172 lines
6 KiB
Java

// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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 google.registry.testing;
import static com.google.common.base.Preconditions.checkState;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import org.junit.rules.ExternalResource;
/**
* JUnit Rule for overriding {@code private static} fields during a test.
*
* <p>This rule uses reflection to change the value of a field while your test
* is running and then restore it to its original value after it's done (even
* if the test fails). The injection will work even if the field is marked
* {@code private} (but not if it's {@code final}). The downside is that if you
* rename the field in the future, Eclipse refactoring won't be smart enough to
* update the injection site.
*
* <p>We encourage you to consider using
* {@link google.registry.util.NonFinalForTesting &#064;NonFinalForTesting}
* to document your injected fields.
*
* <p>This class is a horrible evil hack, but it alleviates you of the toil of
* having to break encapsulation by making your fields non-{@code private}, using
* the {@link com.google.common.annotations.VisibleForTesting &#064;VisibleForTesting}
* annotation to document why you've reduced visibility, creating a temporary field
* to store the old value, and then writing an {@link org.junit.After &#064;After}
* method to restore it. So sometimes it feels good to be evil; but hopefully one
* day we'll be able to delete this class and do things <i>properly</i> with
* <a href="http://square.github.io/dagger/">Dagger</a> dependency injection.
*
* <p>You use this class by declaring it as a {@link org.junit.Rule &#064;Rule}
* field and then calling {@link #setStaticField} from either your {@link
* org.junit.Test &#064;Test} or {@link org.junit.Before &#064;Before} methods. For
* example:
*
* <pre>
* // Doomsday.java
* public class Doomsday {
*
* private static Clock clock = new SystemClock();
*
* public long getTime() {
* return clock.currentTimeMillis();
* }
* }
*
* // DoomsdayTest.java
* &#064;RunWith(JUnit4.class)
* public class DoomsdayTest {
*
* &#064;Rule
* public InjectRule inject = new InjectRule();
*
* private final FakeClock clock = new FakeClock();
*
* &#064;Before
* public void before() {
* inject.setStaticField(Doomsday.class, "clock", clock);
* }
*
* &#064;Test
* public void test() {
* clock.advanceBy(666L);
* Doomsday doom = new Doomsday();
* assertEquals(666L, doom.getTime());
* }
* }
* </pre>
*
* @see google.registry.util.NonFinalForTesting
* @see org.junit.rules.ExternalResource
*/
public class InjectRule extends ExternalResource {
private static class Change {
private final Field field;
@Nullable private final Object oldValue;
@Nullable private final Object newValue;
Change(Field field, @Nullable Object oldValue, @Nullable Object newValue) {
this.field = field;
this.oldValue = oldValue;
this.newValue = newValue;
}
}
private final List<Change> changes = new ArrayList<>();
private final Set<Field> injected = new HashSet<>();
/**
* Sets a static field and be restores its current value after the test completes.
*
* <p>The field is allowed to be {@code private}, but it most not be {@code final}.
*
* <p>This method may be called either from either your {@link org.junit.Before @Before}
* method or from the {@link org.junit.Test @Test} method itself. However you may not
* inject the same field multiple times during the same test.
*
* @throws IllegalArgumentException if the static field could not be found or modified.
* @throws IllegalStateException if the field has already been injected during this test.
*/
public void setStaticField(Class<?> clazz, String fieldName, @Nullable Object newValue) {
Field field;
Object oldValue;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
oldValue = field.get(null);
} catch (NoSuchFieldException
| SecurityException
| IllegalArgumentException
| IllegalAccessException e) {
throw new IllegalArgumentException(String.format(
"Static field not found: %s.%s", clazz.getSimpleName(), fieldName), e);
}
checkState(!injected.contains(field),
"Static field already injected: %s.%s", clazz.getSimpleName(), fieldName);
try {
field.set(null, newValue);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalArgumentException(String.format(
"Static field not settable: %s.%s", clazz.getSimpleName(), fieldName), e);
}
changes.add(new Change(field, oldValue, newValue));
injected.add(field);
}
@Override
protected void after() {
RuntimeException thrown = null;
for (Change change : changes) {
try {
checkState(change.field.get(null).equals(change.newValue),
"Static field value was changed post-injection: %s.%s",
change.field.getDeclaringClass().getSimpleName(), change.field.getName());
change.field.set(null, change.oldValue);
} catch (IllegalArgumentException
| IllegalStateException
| IllegalAccessException e) {
if (thrown == null) {
thrown = new RuntimeException(e);
} else {
thrown.addSuppressed(e);
}
}
}
changes.clear();
injected.clear();
if (thrown != null) {
throw thrown;
}
}
}