mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 20:17:51 +02:00
172 lines
6 KiB
Java
172 lines
6 KiB
Java
// Copyright 2016 The Nomulus 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 @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 @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 @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 @Rule}
|
|
* field and then calling {@link #setStaticField} from either your {@link
|
|
* org.junit.Test @Test} or {@link org.junit.Before @Before} methods. For
|
|
* example:
|
|
*
|
|
* <pre>
|
|
* // Doomsday.java
|
|
* public class Doomsday {
|
|
*
|
|
* private static Clock clock = new SystemClock();
|
|
*
|
|
* public long getTime() {
|
|
* return clock.currentTimeMillis();
|
|
* }
|
|
* }
|
|
*
|
|
* // DoomsdayTest.java
|
|
* @RunWith(JUnit4.class)
|
|
* public class DoomsdayTest {
|
|
*
|
|
* @Rule
|
|
* public InjectRule inject = new InjectRule();
|
|
*
|
|
* private final FakeClock clock = new FakeClock();
|
|
*
|
|
* @Before
|
|
* public void before() {
|
|
* inject.setStaticField(Doomsday.class, "clock", clock);
|
|
* }
|
|
*
|
|
* @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;
|
|
}
|
|
}
|
|
}
|