From 58bfeec99cfce42e5c5aef8a99b61f5c74801e78 Mon Sep 17 00:00:00 2001 From: Christian Brunschen Date: Tue, 4 Jun 2019 14:38:06 +0100 Subject: [PATCH] Support handling multiple static mocks. This CL introduces a "StaticMockFixture" interface that allows setting up the classes to be mocked in a StaticMockitoSessionBuilder and separately setting up the default behaviour of the so-mocked classes, as well as tearing them down. It also introduces a StaticMockFixtureRule that takes some number of StaticMockFixture instances or suppliers thereof, and manages them all within the same StaticMockitoSession and its builder. This in turn allows us to use such a StaticMockFixtureRule to orchestrate the automatic set-up and tear-down of different sets of static mocks, all within the same StatickMockitoSession, since juggling multiple StaticMockitoSessions turns out to be tricky and full of pitfalls. Finally this change refactors TestableDeviceConfig to become a StaticMockFixture which can then be used together with other static mocks, and to separately offer a TestableDeviceConfigRule that can be used when no other static mocks are required to manage the TestableDeviceConfig on its own. Change-Id: Icd88736390699e8956cd3d6923636afc0e015440 Test: included in CL. Bug: 129937393 --- .../android/server/am/AppCompactorTest.java | 3 +- .../testables/MultipleStaticMocksTest.java | 163 +++++++++++++++ .../server/testables/StaticMockFixture.java | 56 ++++++ .../testables/StaticMockFixtureRule.java | 135 +++++++++++++ .../testables/StaticMockFixtureRuleTest.java | 186 ++++++++++++++++++ .../testables/TestableDeviceConfig.java | 81 ++++---- ...leDeviceConfigAndOtherStaticMocksTest.java | 172 ++++++++++++++++ .../testables/TestableDeviceConfigTest.java | 3 +- 8 files changed, 760 insertions(+), 39 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/testables/MultipleStaticMocksTest.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixture.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRule.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRuleTest.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigAndOtherStaticMocksTest.java diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AppCompactorTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AppCompactorTest.java index 475901a75d0dd..4a33739a8596b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/AppCompactorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/AppCompactorTest.java @@ -62,7 +62,8 @@ public final class AppCompactorTest { private CountDownLatch mCountDown; @Rule - public TestableDeviceConfig mDeviceConfig = new TestableDeviceConfig(); + public TestableDeviceConfig.TestableDeviceConfigRule + mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); @Before public void setUp() { diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/MultipleStaticMocksTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/MultipleStaticMocksTest.java new file mode 100644 index 0000000000000..c0ab70a57327f --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/testables/MultipleStaticMocksTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 com.android.server.testables; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MultipleStaticMocksTest { + @Rule + public StaticMockFixtureRule mStaticMockFixtureRule = + new StaticMockFixtureRule(AB::new, CD::new); + + private List mCollected; + + @Test + public void testMultipleStaticMocks() throws Exception { + mCollected = new ArrayList<>(); + int n = 0; + + A.a(); + n = verifyCollected(n, "A.a"); + + D.b(); + n = verifyCollected(n, "D.b"); + + C.b(); + n = verifyCollected(n, "C.b"); + + B.a(); + n = verifyCollected(n, "B.a"); + } + + private int verifyCollected(int n, String... last) { + assertThat(mCollected).hasSize(n + last.length); + assertThat(mCollected.subList(n, mCollected.size())) + .containsExactlyElementsIn(last).inOrder(); + return n + last.length; + } + + private static class A { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class B { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class C { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class D { + /* package */ static void a() {} + /* package */ static void b() {} + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link A} and {@link B}. + */ + private class AB implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(A.class); + sessionBuilder.spyStatic(B.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.a"); + return null; + }).when(A::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.b"); + return null; + }).when(A::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.a"); + return null; + }).when(B::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.b"); + return null; + }).when(B::b); + } + + @Override + public void tearDown() { + + } + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link C} and {@link D}. + */ + private class CD implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(C.class); + sessionBuilder.spyStatic(D.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.a"); + return null; + }).when(C::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.b"); + return null; + }).when(C::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.a"); + return null; + }).when(D::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.b"); + return null; + }).when(D::b); + } + + @Override + public void tearDown() { + + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixture.java b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixture.java new file mode 100644 index 0000000000000..0303fe1e1eb05 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixture.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 com.android.server.testables; + + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +/** + * Provides support for a set of static mocks for use within a single shared + * {@link StaticMockitoSession}. + */ +public interface StaticMockFixture { + /** + * Adds any required mock or spy classes managed by this {@link StaticMockFixture} to the + * {@link StaticMockitoSessionBuilder} provided. + * + * Call this to set up the classes that this expects to be mocked, by adding them to the + * {@link StaticMockitoSessionBuilder} using + * {@link StaticMockitoSessionBuilder#mockStatic(Class)}, + * {@link StaticMockitoSessionBuilder#spyStatic(Class)} or similar as appropriate. + * + * @param sessionBuilder the {@link StaticMockitoSessionBuilder} to which the classes should be + * added to mock, spy, or otherwise as required + * @return sessionBuilder, to allow for fluent programming + */ + StaticMockitoSessionBuilder setUpMockedClasses(StaticMockitoSessionBuilder sessionBuilder); + + /** + * Configures the behaviours of any mock or spy classes managed by this + * {@link StaticMockFixture}. + * + * Call this after {@link StaticMockitoSessionBuilder#startMocking()} has been called. + * This sets up any default behaviors for the mocks, spys, etc. + */ + void setUpMockBehaviors(); + + /** + * Tear everything down. + */ + void tearDown(); +} diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRule.java b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRule.java new file mode 100644 index 0000000000000..8e9d7ee71df3b --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRule.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 com.android.server.testables; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.quality.Strictness; + +import java.util.function.Supplier; + +/** + *

StaticMockFixtureRule is a {@link TestRule} that wraps one or more {@link StaticMockFixture}s + * to set them up and tear it down automatically. This works well when you have no other static + * mocks than the ones supported by their respective {@link StaticMockFixture}s.

+ * + *

StaticMockFixtureRule should be defined as a rule on your test so it can clean up after + * itself. Like the following:

+ *
+*  public final StaticMockFixture mStaticMockFixtures = ...;
+ * @Rule
+ * public final StaticMockFixtureRule mStaticMockFixtureRule =
+ *     new StaticMockFixtureRule(mStaticMockFixtures);
+ * 
+ */ +public class StaticMockFixtureRule implements TestRule { + private StaticMockitoSession mMockitoSession; + private StaticMockFixture[] mStaticMockFixtures; + private Supplier[] mSupplier; + + /** + * Constructs a StaticMockFixtureRule that always uses the same {@link StaticMockFixture} + * instance(s). + * + * @param staticMockFixtures the {@link StaticMockFixture}(s) to use. + */ + public StaticMockFixtureRule(StaticMockFixture... staticMockFixtures) { + mStaticMockFixtures = staticMockFixtures; + mSupplier = null; + } + + /** + * Constructs a StaticMockFixtureRule that retrieves a new {@link StaticMockFixture} instance + * from one or more {@link Supplier}s for each test invocation. + * + * @param supplier the {@link Supplier}(s) that will supply the + * {@link StaticMockFixture}(s). + */ + @SafeVarargs + public StaticMockFixtureRule(Supplier... supplier) { + mStaticMockFixtures = null; + mSupplier = supplier; + } + + @Override + public Statement apply(Statement base, Description description) { + StaticMockitoSessionBuilder sessionBuilder = getSessionBuilder(); + + if (mSupplier != null) { + mStaticMockFixtures = new StaticMockFixture[mSupplier.length]; + for (int i = 0; i < mSupplier.length; i++) { + mStaticMockFixtures[i] = mSupplier[i].get(); + } + } + + for (int i = 0; i < mStaticMockFixtures.length; i++) { + sessionBuilder = mStaticMockFixtures[i].setUpMockedClasses(sessionBuilder); + } + + mMockitoSession = sessionBuilder.startMocking(); + + for (int i = 0; i < mStaticMockFixtures.length; i++) { + mStaticMockFixtures[i].setUpMockBehaviors(); + } + + return new TestWatcher() { + @Override + protected void succeeded(Description description) { + tearDown(null); + } + + @Override + protected void failed(Throwable e, Description description) { + tearDown(e); + } + }.apply(base, description); + } + + /** + * This allows overriding the creation of the builder for a new {@link StaticMockitoSession}. + * Mainly for testing, but also useful if you have other requirements for the session. + * + * @return a new {@link StaticMockitoSessionBuilder}. + */ + public StaticMockitoSessionBuilder getSessionBuilder() { + return mockitoSession().strictness(Strictness.LENIENT); + } + + private void tearDown(Throwable e) { + mMockitoSession.finishMocking(e); + + for (int i = mStaticMockFixtures.length - 1; i >= 0; i--) { + mStaticMockFixtures[i].tearDown(); + if (mSupplier != null) { + mStaticMockFixtures[i] = null; + } + } + + if (mSupplier != null) { + mStaticMockFixtures = null; + } + + mMockitoSession = null; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRuleTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRuleTest.java new file mode 100644 index 0000000000000..b7e71ded30ab6 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/testables/StaticMockFixtureRuleTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 com.android.server.testables; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; + +import java.util.function.Supplier; + +/** Tests that StaticMockFixture manages fixtures and suppliers correctly. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class StaticMockFixtureRuleTest { + private MockitoSession mMockitoSession; + + @Mock private StaticMockitoSessionBuilder mSessionBuilder; + @Mock private StaticMockitoSession mSession; + @Mock private StaticMockFixture mA1; + @Mock private StaticMockFixture mB1; + @Mock private StaticMockFixture mA2; + @Mock private StaticMockFixture mB2; + @Mock private Supplier mSupplyA; + @Mock private Supplier mSupplyB; + @Mock private Statement mStatement; + @Mock private Description mDescription; + + @Before + public void setUp() throws Throwable { + mMockitoSession = Mockito.mockitoSession() + .strictness(LENIENT) + .initMocks(this) + .startMocking(); + prepareMockBehaviours(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + private void prepareFixtureMocks(StaticMockFixture... mocks) { + for (StaticMockFixture mock : mocks) { + when(mock.setUpMockedClasses(any())).thenAnswer( + invocation -> invocation.getArgument(0)); + doNothing().when(mock).setUpMockBehaviors(); + } + } + + private void prepareMockBehaviours() throws Throwable { + when(mSessionBuilder.startMocking()).thenReturn(mSession); + when(mSupplyA.get()).thenReturn(mA1, mA2); + when(mSupplyB.get()).thenReturn(mB1, mB2); + prepareFixtureMocks(mA1, mA2, mB1, mB2); + when(mA1.setUpMockedClasses(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doNothing().when(mA1).setUpMockBehaviors(); + when(mB1.setUpMockedClasses(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doNothing().when(mB1).setUpMockBehaviors(); + doNothing().when(mStatement).evaluate(); + doNothing().when(mA1).tearDown(); + doNothing().when(mB1).tearDown(); + } + + private InOrder mocksInOrder() { + return inOrder(mSessionBuilder, mSession, mSupplyA, mSupplyB, + mA1, mA2, mB1, mB2, mStatement, mDescription); + } + + private void verifyNoMoreImportantMockInteractions() { + verifyNoMoreInteractions(mSupplyA, mSupplyB, mA1, mA2, mB1, mB2, mStatement); + } + + @Test + public void testRuleWorksWithExplicitFixtures() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mA1, mB1) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement runMe = rule.apply(mStatement, mDescription); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + runMe.evaluate(); + + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + // Round two: use the same fixtures again. + rule.apply(mStatement, mDescription).evaluate(); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } + + @Test + public void testRuleWorksWithFixtureSuppliers() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mSupplyA, mSupplyB) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement runMe = rule.apply(mStatement, mDescription); + + inOrder.verify(mSupplyA).get(); + inOrder.verify(mSupplyB).get(); + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + runMe.evaluate(); + + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + // Round two: use the same suppliers again to retrieve different fixtures: mA2 and mB2 + rule.apply(mStatement, mDescription).evaluate(); + + inOrder.verify(mSupplyA).get(); + inOrder.verify(mSupplyB).get(); + inOrder.verify(mA2).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB2).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA2).setUpMockBehaviors(); + inOrder.verify(mB2).setUpMockBehaviors(); + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB2).tearDown(); + inOrder.verify(mA2).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java index 6d8fb731df9ff..32631be3ec24b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java @@ -17,7 +17,6 @@ package com.android.server.testables; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -31,14 +30,10 @@ import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.util.Pair; -import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; import org.junit.rules.TestRule; -import org.junit.rules.TestWatcher; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; import org.mockito.Mockito; -import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; import java.util.Collections; @@ -48,20 +43,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; /** - * TestableDeviceConfig uses ExtendedMockito to replace the real implementation of DeviceConfig - * with essentially a local HashMap in the callers process. This allows for unit testing that do not - * modify the real DeviceConfig on the device at all. - * - *

TestableDeviceConfig should be defined as a rule on your test so it can clean up after itself. - * Like the following:

- *
- * @Rule
- * public final TestableDeviceConfig mTestableDeviceConfig = new TestableDeviceConfig();
- * 
+ * TestableDeviceConfig is a {@link StaticMockFixture} that uses ExtendedMockito to replace the real + * implementation of DeviceConfig with essentially a local HashMap in the callers process. This + * allows for unit testing that do not modify the real DeviceConfig on the device at all. */ -public final class TestableDeviceConfig implements TestRule { +public final class TestableDeviceConfig implements StaticMockFixture { - private StaticMockitoSession mMockitoSession; private Map> mOnPropertiesChangedListenerMap = new HashMap<>(); private Map mKeyValueMap = new ConcurrentHashMap<>(); @@ -73,14 +60,21 @@ public final class TestableDeviceConfig implements TestRule { mKeyValueMap.clear(); } + /** + * {@inheritDoc} + */ @Override - public Statement apply(Statement base, Description description) { - mMockitoSession = mockitoSession() - .initMocks(this) - .strictness(Strictness.LENIENT) - .spyStatic(DeviceConfig.class) - .startMocking(); + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(DeviceConfig.class); + return sessionBuilder; + } + /** + * {@inheritDoc} + */ + @Override + public void setUpMockBehaviors() { doAnswer((Answer) invocationOnMock -> { String namespace = invocationOnMock.getArgument(0); Executor executor = invocationOnMock.getArgument(1); @@ -115,20 +109,15 @@ public final class TestableDeviceConfig implements TestRule { String name = invocationOnMock.getArgument(1); return mKeyValueMap.get(getKey(namespace, name)); }).when(() -> DeviceConfig.getProperty(anyString(), anyString())); + } - return new TestWatcher() { - @Override - protected void succeeded(Description description) { - mMockitoSession.finishMocking(); - mOnPropertiesChangedListenerMap.clear(); - } - - @Override - protected void failed(Throwable e, Description description) { - mMockitoSession.finishMocking(e); - mOnPropertiesChangedListenerMap.clear(); - } - }.apply(base, description); + /** + * {@inheritDoc} + */ + @Override + public void tearDown() { + clearDeviceConfig(); + mOnPropertiesChangedListenerMap.clear(); } private static String getKey(String namespace, String name) { @@ -210,4 +199,22 @@ public final class TestableDeviceConfig implements TestRule { return properties; } + /** + *

TestableDeviceConfigRule is a {@link TestRule} that wraps a {@link TestableDeviceConfig} + * to set it up and tear it down automatically. This works well when you have no other static + * mocks.

+ * + *

TestableDeviceConfigRule should be defined as a rule on your test so it can clean up after + * itself. Like the following:

+ *
+     * @Rule
+     * public final TestableDeviceConfigRule mTestableDeviceConfigRule =
+     *     new TestableDeviceConfigRule();
+     * 
+ */ + public static class TestableDeviceConfigRule extends StaticMockFixtureRule { + public TestableDeviceConfigRule() { + super(TestableDeviceConfig::new); + } + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigAndOtherStaticMocksTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigAndOtherStaticMocksTest.java new file mode 100644 index 0000000000000..9616d13e42fea --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigAndOtherStaticMocksTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 com.android.server.testables; + +import android.provider.DeviceConfig; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TestableDeviceConfigAndOtherStaticMocksTest { + @Rule + public StaticMockFixtureRule mStaticMockFixtureRule = + new StaticMockFixtureRule(TestableDeviceConfig::new, AB::new, CD::new); + + private List mCollected; + + @Test + public void testDeviceConfigAndOtherStaticMocks() throws Exception { + mCollected = new ArrayList<>(); + int n = 0; + + String namespace = "foo"; + String flag = "bar"; + String flagValue = "new value"; + + Assert.assertNull(DeviceConfig.getProperty(namespace, flag)); + + A.a(); + verifyCollected(++n, "A.a"); + + DeviceConfig.setProperty(namespace, flag, flagValue, false); + + D.b(); + verifyCollected(++n, "D.b"); + + Assert.assertEquals(flagValue, DeviceConfig.getProperty(namespace, flag)); + + C.b(); + verifyCollected(++n, "C.b"); + + B.a(); + verifyCollected(++n, "B.a"); + } + + private void verifyCollected(int n, String last) { + Assert.assertEquals(n, mCollected.size()); + Assert.assertEquals(last, mCollected.get(n - 1)); + } + + private static class A { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class B { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class C { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class D { + /* package */ static void a() {} + /* package */ static void b() {} + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link A} and {@link B}. + */ + private class AB implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(A.class); + sessionBuilder.spyStatic(B.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.a"); + return null; + }).when(A::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.b"); + return null; + }).when(A::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.a"); + return null; + }).when(B::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.b"); + return null; + }).when(B::b); + } + + @Override + public void tearDown() { + + } + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link C} and {@link D}. + */ + private class CD implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(C.class); + sessionBuilder.spyStatic(D.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.a"); + return null; + }).when(C::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.b"); + return null; + }).when(C::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.a"); + return null; + }).when(D::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.b"); + return null; + }).when(D::b); + } + + @Override + public void tearDown() { + + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java index 3eb72097182dc..d76c9388cacfc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java @@ -45,7 +45,8 @@ public class TestableDeviceConfigTest { private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec @Rule - public TestableDeviceConfig mTestableDeviceConfig = new TestableDeviceConfig(); + public TestableDeviceConfig.TestableDeviceConfigRule + mTestableDeviceConfig = new TestableDeviceConfig.TestableDeviceConfigRule(); @Test public void getProperty_empty() {