Testing is one of the most important parts of developing high-quality software products. Today we will talk about some methodologies and libraries developed and used by our team to write tests for Android applications.
Let's start with the most basic things, because more experienced developers can go directly to the section on tools for UI testing. For those who want to learn or refresh basic things - enjoy reading.
Let's create a small component, which we will test. It parses the file with a JSON object containing the name and returns the resulting string:
public class NameRepository { private final File file; public NameRepository(File file) { this.file = file; } public String getName() throws IOException { Gson gson = new Gson(); User user = gson.fromJson(readFile(), User.class); return user.name; } public String readFile() throws IOException { byte[] bytes = new byte[(int) file.length()]; try (FileInputStream in = new FileInputStream(file)) { in.read(bytes); } return new String(bytes, Charset.defaultCharset()); } private static final class User { String name; } }
Here and in the future I will give an abbreviated version of the code. The full version can be viewed in the repository . Each snippet will be accompanied by a link to the full code .
Now we will write the first JUnit test. JUnit is a Java library for writing tests. In order for JUnit to know that a method is a test, you need to add the @Test
annotation to it. JUnit contains the Assert
class, which allows you to compare actual values with expected values and displays an error if the values do not match. This test will test the correctness of our component, namely, reading the file, parsing the JSON and getting the correct field:
public class NameRepositoryTest { private static final File FILE = new File("test_file"); NameRepository nameRepository = new NameRepository(FILE); @Test public void getName_isSasha() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); FILE.delete(); } @Test public void getName_notMia() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); String name = nameRepository.getName(); Assert.assertNotEquals(name, "Mia"); FILE.delete(); } }
Tests are also code that must be supported. Moreover, the test code should be easy to understand so that it can be verified in the mind. Therefore, it makes sense to invest in simplifying the test code, getting rid of duplication and improving readability. Let's look at the widely used libraries that will help us in this matter.
In order not to duplicate the preparation code in each test, there are annotations @Before
and @After
. Methods marked with @Before
annotation will be executed before each test, and marked with @After
annotation after each test. There are also annotations @BeforeClass
and @AfterClass
, which are executed respectively before and after all the tests in the class. Let's redo our test using these methods:
public class NameRepositoryTest { private static final File FILE = new File("test_file"); NameRepository nameRepository = new NameRepository(FILE); @Before public void setUp() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); } @After public void tearDown() { FILE.delete(); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } @Test public void getName_notMia() throws Exception { String name = nameRepository.getName(); Assert.assertNotEquals(name, "Mia"); } }
We were able to remove duplication of the setup code for each test. However, many different classes with tests may require creating a file, and I would also like to remove this duplication. For this there is a library of test rules ( TestRule ). The test rule performs a function similar to @Before
and @After
. In the apply () method of this class, we can perform the actions we need before and after the execution of each or all tests. In addition to reducing code duplication, the advantage of such a method is that the code is removed from the class of tests, which reduces the amount of code in the test and makes it easier to read. Let's write a rule to create a file:
public class CreateFileRule implements TestRule { private final File file; private final String text; public CreateFileRule(File file, String text) { this.file = file; this.text = text; } @Override public Statement apply(final Statement s, Description d) { return new Statement() { @Override public void evaluate() throws Throwable { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream(FILE), UTF_8)), true); writer.println(text); writer.close(); try { s.evaluate(); } finally { file.delete(); } } }; } }
We use this rule in our test. In order for the TestRule
actions to TestRule
performed for each test, you must mark the TestRule
annotation.
public class NameRepositoryTest { static final File FILE = new File("test_file"); @Rule public final CreateFileRule fileRule = new CreateFileRule(FILE, "{name : Sasha}"); NameRepository nameRepository = new NameRepository(new FileReader(FILE)); @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } ... }
If the rule is marked with the @ClassRule
annotation, then the actions will not be invoked before each test, but once before all the tests in the class, similar to the annotations @BeforeClass
and @AfterClass
.
When several TestRule
are used in tests, it may be necessary to start them in a specific order, for this there is a RuleChain with which you can determine the order in which our TestRule
. Create a rule that should create a folder before the file is created:
public class CreateDirRule implements TestRule { private final File dir; public CreateDirRule(File dir) { this.dir = dir; } @Override public Statement apply(final Statement s, Description d) { return new Statement() { @Override public void evaluate() throws Throwable { dir.mkdir(); try { s.evaluate(); } finally { dir.delete(); } } }; } }
With this rule, the class with the dough will look like this:
public class NameRepositoryTest { static final File DIR = new File("test_dir"); static final File FILE = Paths.get(DIR.toString(), "test_file").toFile(); @Rule public final RuleChain chain = RuleChain .outerRule(new CreateDirRule(DIR)) .around(new CreateFileRule(FILE, "{name : Sasha}")); @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } ... }
Now in each test the directory will be created before creating the file and deleted after deleting the file.
Google Truth is a library for improving the readability of test code. Contains assert methods (similar to JUnit Assert ), but more readable for humans, and also includes many more options for checking parameters. This is the previous test using Truth:
@Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } @Test public void getName_notMia() throws Exception { String name = nameRepository.getName(); assertThat(name).isNotEqualTo("Mia"); }
It can be seen that the code reads almost like text in spoken English.
Our component does two different jobs: it reads a file and parses it. To adhere to the principle of sole responsibility, let's separate the logic of reading a file into a separate component:
public class FileReader { private final File file; public FileReader(File file) { this.file = file; } public String readFile() throws IOException { byte[] bytes = new byte[(int) file.length()]; try (FileInputStream in = new FileInputStream(file)) { in.read(bytes); } return new String(bytes, Charset.defaultCharset()); } }
Now we want to test NameRepository
, and in fact we are testing and reading the file in FileReader
. To avoid this and thereby increase the isolation, reliability and speed of the test, we can replace the real FileReader
with his MoK.
Mockito is a library for creating stubs (mocks) instead of real objects for use in tests. Some actions that can be performed using the Mockito:
create stubs for classes and interfaces;
check method calls and values passed to this method;
connecting to a real spy object “spy” to control call methods.
Create a FileReader
mock and configure it so that the readFile()
method returns the string we need:
public class NameRepositoryTest { FileReader fileReader = mock(FileReader.class); NameRepository nameRepository = new NameRepository(fileReader); @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } }
Now there is no file reading. Instead, the mock returns the value set in the test.
The use of mocks has its advantages:
and disadvantages:
There is an easier and more convenient way to create mocks - use the @Mock
special annotation:
@Mock File file;
There are three ways to initialize such mocks:
@Before public void setUp() { MockitoAnnotations.initMocks(this); }
@RunWith(MockitoJUnitRunner.class)
@Rule public final MockitoRule rule = MockitoJUnit.rule();
The second option is maximally declarative and compact, but requires the use of a special test runner, which is not always convenient. The latter option lacks this drawback and is more declarative than using the initMocks()
method.
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { @Mock FileReader fileReader; NameRepository nameRepository; @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); nameRepository = new NameRepository(fileReader); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } }
Android tests can be divided into two types: those that can be run on a regular Java VM, and those that need to be run on an Android Java VM. Let's look at both types of tests.
Tests for code that does not require the operation of Android API components, for which you need an Android emulator or a real device, can be run directly on your computer and on any Java machine. These are mostly business logic unit tests that test a single class in isolation. The integration tests are much less often written, since it is not always possible to create real class objects with which the tested class interacts.
To write a class with Host Java tests, you need the java file to have the path ${moduleName}/src/test/java/...
Also using @RunWith
annotations to specify Runner
, which is responsible for running the tests, the correct call and handling of all methods:
@RunWith(MockitoJUnitRunner.class) public class TestClass {...}
Using these tests has many advantages:
on the other hand, these tests:
In order to be able to use the Android API classes in the Host Java tests, there is a Robolectric library that emulates the Android environment and gives access to its main functions. However, testing Android classes with Roboelectric is often unstable: it takes time while Robolectric supports the latest Android API, there are problems with getting resources, etc. Therefore, real classes are almost not used, and their mocks are used for unit testing.
To run tests using Roboelectric, you need to install a custom TestRunner . In it, you can configure the SDK version (the most recent stable version is 23), designate the main class Application
and other parameters for the emulated Android environment.
public class MainApplication extends Application {}
@RunWith(RobolectricTestRunner.class) @Config(sdk = 21, application = MainApplication.class) public class MainApplicationTest { @Test public void packageName() { assertThat(RuntimeEnvironment.application) .isInstanceOf(MainApplication.class); } }
For instrumental tests, the presence of a device or emulator is mandatory, since we will test button presses, text input, and other actions.
To write a test for an Android Java VM, you need to put a java file along the path ${moduleName}/src/androidTest/java/...
, as well as using @RunWith
annotations to specify AndroidJUnit4
, which will allow you to run tests on an Android device.
@RunWith(AndroidJUnit4.class) public class TestClass {...}
For testing the UI, the Espresso framework is used, which provides an API for testing the user interface of the program. In Espresso, tests run in the background thread and interact with UI elements in the UI thread. Espresso has several basic classes for testing:
Let's write the simplest Android application, which we will test:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); } }
We are testing our application. When testing the UI, you first need to start the Activity. For this, there is an ActivityTestRule that launches an Activity before each test and closes after:
@Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
Let's write a simple test that checks that the element with id R.id.container
shown on the screen:
@RunWith(AndroidJUnit4.class) public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class); @Test public void checkContainerIsDisplayed() { onView(ViewMatchers.withId(R.id.container)) .check(matches(isDisplayed())); } }
Emulator on weak or loaded machines may be slow. Therefore, between the launch of the emulator and the end of the build with the installation of the application on the emulator, it may take enough time for the screen to be blocked from inactivity. Thus, the test can be run when the screen is locked, causing a java.lang.RuntimeException: Could not launch activity within 45 seconds
error java.lang.RuntimeException: Could not launch activity within 45 seconds
. Therefore, before launching the Activity, you need to unlock and turn on the screen. Once this needs to be done in each UI test, to avoid duplication of the code, we will create a rule that will unlock and turn on the screen before the test:
class UnlockScreenRule<A extends AppCompatActivity> implements TestRule { ActivityTestRule<A> activityRule; UnlockScreenRule(ActivityTestRule<A> activityRule) { this.activityRule = activityRule; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { activityRule.runOnUiThread(() -> activityRule .getActivity() .getWindow() .addFlags( WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)); statement.evaluate(); } }; } }
Let's write a custom ActivityTestRule
that unlocks the emulator screen and starts activating before running the tests:
public class ActivityTestRule<A extends AppCompatActivity> implements TestRule { private final android.support.test.rule.ActivityTestRule<A> activityRule; private final RuleChain ruleChain; public ActivityTestRule(Class<A> activityClass) { this.activityRule = new ActivityTestRule<>(activityClass, true, true); ruleChain = RuleChain .outerRule(activityRule) .around(new UnlockScreenRule(activityRule)); } public android.support.test.rule.ActivityTestRule<A> getActivityRule() { return activityRule; } public void runOnUiThread(Runnable runnable) throws Throwable { activityRule.runOnUiThread(runnable); } public A getActivity() { return activityRule.getActivity(); } @Override public Statement apply(Statement statement, Description description) { return ruleChain.apply(statement, description); } }
Using this rule instead of the standard one can greatly reduce the number of random drops in UI tests in CI.
Typically, the layout and logic of the UI application is not put all in the activit, but is divided into windows, for each of which a fragment is created. Let's create a simple snippet to display the name using the NameRepository
:
public class UserFragment extends Fragment { private TextView textView; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); try { textView.setText(createNameRepository().getName()); } catch (IOException exception) { throw new RuntimeException(exception); } return textView; } private NameRepository createNameRepository() { return new NameRepository( new FileReader( new File( getContext().getFilesDir().getAbsoluteFile() + File.separator + "test_file"))); } @Override public void onDestroyView() { super.onDestroyView(); textView = null; } }
When opening a fragment, the UI may freeze for a while, and if animations of transitions between fragments are used, the test may begin before the fragment appears. Therefore, you need not just to open the fragment, but to wait until it is launched. The Awaitility library, which has a very simple and clear syntax, is excellent for waiting for the result of actions. Let us write a rule that starts a fragment and waits to start it using this library:
class OpenFragmentRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final Fragment fragment; OpenFragmentRule(ActivityTestRule<A> activityRule, Fragment fragment) { this.activityRule = activityRule; this.fragment = fragment; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { openFragment(fragment); await().atMost(5, SECONDS).until(fragment::isResumed); statement.evaluate(); } }; } }
In this case, the expression means that if the fragment does not start within five seconds, the test will not be passed. It should be noted that as soon as the fragment starts, the test will immediately continue execution and will not wait all five seconds.
Similar to the rule that triggers activations, it is logical to create a rule that triggers a fragment:
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment> implements TestRule { private ActivityTestRule<A> activityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule(Class<A> activityClass, F fragment) { this.fragment = fragment; this.activityRule = new ActivityTestRule<>(activityClass); ruleChain = RuleChain .outerRule(activityRule) .around(new OpenFragmentRule<>(activityRule, fragment)); } public ActivityTestRule<A> getActivityRule() { return activityRule; } public F getFragment() { return fragment; } public void runOnUiThread(Runnable runnable) throws Throwable { activityRule.runOnUiThread(runnable); } public A getActivity() { return activityRule.getActivity(); } @Override public Statement apply(Statement statement, Description description) { return ruleChain.apply(statement, description); } }
A fragment test using this rule will look like this:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @Rule public final RuleChain rules = RuleChain .outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}")) .around(new FragmentTestRule<>(MainActivity.class, new UserFragment())); @Test public void nameDisplayed() { onView(withText("Sasha")).check(matches(isDisplayed())); } private File getTestFile() { return new File( InstrumentationRegistry.getTargetContext() .getFilesDir() .getAbsoluteFile() + File.separator + "test_file"); } }
Since disk operations, namely, retrieving a name from a file, can take a relatively long time, this operation should be performed asynchronously. For asynchronous retrieval of the name from the file, use the RxJava library. You can confidently say that RxJava is now used in most Android applications. Virtually every task that needs to be performed asynchronously is performed using RxJava, because this is probably one of the most convenient and understandable libraries for asynchronous code execution.
Let's change our repository so that it works asynchronously:
public class NameRepository { ... public Single<String> getName() { return Single.create( emitter -> { Gson gson = new Gson(); emitter.onSuccess( gson.fromJson(fileReader.readFile(), User.class).getName()); }); } }
To test the RX code, there is a special class TestObserver
, which automatically subscribes to Observable
and instantly gets the result. The test repository will look like this:
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { ... @Test public void getName() { TestObserver<String> observer = nameRepository.getName().test(); observer.assertValue("Sasha"); } }
Update our snippet using the new reactive repository:
public class UserFragment extends Fragment { ... @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); createNameRepository() .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } }
, Awaitility:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { ... @Test public void nameDisplayed() { await() .atMost(5, SECONDS) .ignoreExceptions() .untilAsserted( () -> onView(ViewMatchers.withText("Sasha")) .check(matches(isDisplayed()))); } }
, — , , , . , , textView
null
. NullPointerException
textView
subscribe()
, :
public class UserFragment extends Fragment { private TextView textView; private Disposable disposable; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); disposable = createNameRepository() .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } @Override public void onDestroyView() { super.onDestroyView(); disposable.dispose(); textView = null; } }
, , . . onCreateView
textView
null
, . :
public class FragmentAsyncTestRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final Fragment fragment; public FragmentAsyncTestRule(Class<A> activityClass, Fragment fragment) { this.activityRule = new ActivityTestRule<>(activityClass); this.fragment = fragment; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } finally { activityRule.launchActivity(new Intent()); openFragment(fragment); openFragment(new Fragment()); } } }; } }
:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); ... }
, .
, Observable
, timeout
:
public class UserPresenter { public interface Listener { void onUserNameLoaded(String name); void onGettingUserNameError(String message); } private final Listener listener; private final NameRepository nameRepository; public UserPresenter(Listener listener, NameRepository nameRepository) { this.listener = listener; this.nameRepository = nameRepository; } public void getUserName() { nameRepository .getName() .timeout(2, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( listener::onUserNameLoaded, error -> listener.onGettingUserNameError(error.getMessage())); } }
, . :
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(Observable.just("Sasha")); presenter = new UserPresenter(listener, nameRepository); } @Test public void getUserName() { presenter.getUserName(); verifyNoMoreInteractions(listener); } }
listener
, , . Awaitility . - , RxJava Schedulers
. TestScheduler , , Observable
, . , :
public class RxImmediateSchedulerRule implements TestRule { private static final TestScheduler TEST_SCHEDULER = new TestScheduler(); private static final Scheduler IMMEDIATE_SCHEDULER = new Scheduler() { @Override public Disposable scheduleDirect(Runnable run, long delay, TimeUnit unit) { return super.scheduleDirect(run, 0, unit); } @Override public Worker createWorker() { return new ExecutorScheduler.ExecutorWorker(Runnable::run); } }; @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { RxJavaPlugins.setIoSchedulerHandler(scheduler -> TEST_SCHEDULER); RxJavaPlugins.setComputationSchedulerHandler( scheduler -> TEST_SCHEDULER); RxJavaPlugins.setNewThreadSchedulerHandler( scheduler -> TEST_SCHEDULER); RxAndroidPlugins.setMainThreadSchedulerHandler( scheduler -> IMMEDIATE_SCHEDULER); try { base.evaluate(); } finally { RxJavaPlugins.reset(); RxAndroidPlugins.reset(); } } }; } public TestScheduler getTestScheduler() { return TEST_SCHEDULER; } }
:
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { static final int TIMEOUT_SEC = 2; static final String NAME = "Sasha"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository); } @Test public void getUserName() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onUserNameLoaded(NAME); } @Test public void getUserName_timeout() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onGettingUserNameError(any()); } }
Dependency Injection . Dagger 2 — , . Android Dagger. , , , .
, Dagger ApplicationComponent
, , Application
, , , .
public class MainApplication extends Application { private ApplicationComponent component; @Override public void onCreate() { super.onCreate(); component = DaggerApplicationComponent.builder() .contextModule(new ContextModule(this)) .build(); } public ApplicationComponent getComponent() { return component; } }
Dagger , :
@Module public class UserModule { @Provides NameRepository provideNameRepository(@Private FileReader fileReader) { return new NameRepository(fileReader); } @Private @Provides FileReader provideFileReader(@Private File file) { return new FileReader(file); } @Private @Provides File provideFile(Context context) { return new File(context.getFilesDir().getAbsoluteFile() + File.separator + "test_file"); } @Qualifier @Retention(RetentionPolicy.RUNTIME) private @interface Private {} }
, Dagger:
public class UserFragment extends Fragment { ... @Inject NameRepository nameRepository; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((MainApplication) getActivity().getApplication()) .getComponent() .createUserComponent() .injectsUserFragment(this); textView = new TextView(getActivity()); disposable = nameRepository .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } }
UI unit- . Dagger, ApplicationComponent
. Application
:
public void setComponentForTest(ApplicationComponent component) { this.component = component; }
, :
class TestDaggerComponentRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final ApplicationComponent component; TestDaggerComponentRule( ActivityTestRule<A> activityRule, ApplicationComponent component) { this.activityRule = activityRule; this.component = component; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { MainApplication application = ((MainApplication) activityRule.getActivity().getApplication()); ApplicationComponent originalComponent = application.getComponent(); application.setComponentForTest(component); try { statement.evaluate(); } finally { application.setComponentForTest(originalComponent); } } }; } }
, , Application . , . , , Dagger , .
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment> implements TestRule { private ActivityTestRule<A> activityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule( Class<A> activityClass, F fragment, ApplicationComponent component) { this.fragment = fragment; this.activityRule = new ActivityTestRule<>(activityClass); ruleChain = RuleChain .outerRule(activityRule) .around(new TestDaggerComponentRule<>(activityRule, component)) .around(new OpenFragmentRule<>(activityRule, fragment)); } ... }
:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { ... @Rule public final FragmentTestRule<MainActivity, UserFragment> fragmentRule = new FragmentTestRule<>( MainActivity.class, new UserFragment(), createTestApplicationComponent()); private ApplicationComponent createTestApplicationComponent() { ApplicationComponent component = mock(ApplicationComponent.class); when(component.createUserComponent()) .thenReturn(DaggerUserFragmentTest_TestUserComponent.create()); return component; } @Singleton @Component(modules = {TestUserModule.class}) interface TestUserComponent extends UserComponent {} @Module static class TestUserModule { @Provides public NameRepository provideNameRepository() { NameRepository nameRepository = mock(NameRepository.class); when(nameRepository.getName()).thenReturn( Single.fromCallable(() -> "Sasha")); return nameRepository; } } }
, UI, debug. , debug , :
class UserPresenter { ... public void getUserName() { nameRepository .getName() .timeout(TIMEOUT_SEC, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( name -> { listener.onUserNameLoaded(name); if (BuildConfig.DEBUG) { logger.info(String.format("Name loaded: %s", name)); } }, error -> listener.onGettingUserNameError(error.getMessage())); } }
, . DebugTestRule
, :
public class DebugRule implements TestRule { @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { if (BuildConfig.DEBUG) { base.evaluate(); } } }; } }
:
class UserPresenterDebugTest { ... @Rule public final DebugTestsRule debugRule = new DebugTestsRule(); @Test public void userNameLogged() { presenter.getUserName(); timeoutRule.getTestScheduler().triggerActions(); nameObservable.onNext(NAME); verify(logger).info(contains(NAME)); } }
, TestRule , , , . , .
, .
public class NameRepository { private final FileReader fileReader; public NameRepository(FileReader fileReader) { this.fileReader = fileReader; } public Single<String> getName() { return Single.create( emitter -> { Gson gson = new Gson(); emitter.onSuccess( gson.fromJson(fileReader.readFile(), User.class).name); }); } private static final class User { String name; } }
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { @Mock FileReader fileReader; NameRepository nameRepository; @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); nameRepository = new NameRepository(fileReader); } @Test public void getName() { TestObserver<String> observer = nameRepository.getName().test(); observer.assertValue("Sasha"); } }
public class UserPresenter { public interface Listener { void onUserNameLoaded(String name); void onGettingUserNameError(String message); } private final Listener listener; private final NameRepository nameRepository; private final Logger logger; private Disposable disposable; public UserPresenter( Listener listener, NameRepository nameRepository, Logger logger) { this.listener = listener; this.nameRepository = nameRepository; this.logger = logger; } public void getUserName() { disposable = nameRepository .getName() .timeout(2, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( name -> { listener.onUserNameLoaded(name); if (BuildConfig.DEBUG) { logger.info(String.format("Name loaded: %s", name)); } }, error -> listener.onGettingUserNameError(error.getMessage())); } public void stopLoading() { disposable.dispose(); } }
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { static final int TIMEOUT_SEC = 2; static final String NAME = "Sasha"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; @Mock Logger logger; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository, logger); } @Test public void getUserName() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onUserNameLoaded(NAME); } @Test public void getUserName_timeout() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onGettingUserNameError(any()); } }
@RunWith(RobolectricTestRunner.class) public class UserPresenterDebugTest { private static final String NAME = "Sasha"; @Rule public final DebugRule debugRule = new DebugRule(); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; @Mock Logger logger; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository, logger); } @Test public void userNameLogged() { presenter.getUserName(); timeoutRule.getTestScheduler().triggerActions(); nameObservable.onNext(NAME); verify(logger).info(contains(NAME)); } }
public class UserFragment extends Fragment implements UserPresenter.Listener { private TextView textView; @Inject UserPresenter userPresenter; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((MainApplication) getActivity().getApplication()) .getComponent() .createUserComponent(new UserModule(this)) .injectsUserFragment(this); textView = new TextView(getActivity()); userPresenter.getUserName(); return textView; } @Override public void onUserNameLoaded(String name) { textView.setText(name); } @Override public void onGettingUserNameError(String message) { textView.setText(message); } @Override public void onDestroyView() { super.onDestroyView(); userPresenter.stopLoading(); textView = null; } }
@RunWith(AndroidJUnit4.class) public class UserFragmentIntegrationTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); @Rule public final RuleChain rules = RuleChain .outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}")) .around(new FragmentTestRule<>(MainActivity.class, new UserFragment())); @Test public void nameDisplayed() { await() .atMost(5, SECONDS) .ignoreExceptions() .untilAsserted( () -> onView(ViewMatchers.withText("Sasha")) .check(matches(isDisplayed()))); } private static File getTestFile() { return new File( InstrumentationRegistry.getTargetContext() .getFilesDir() .getAbsoluteFile() + File.separator + "test_file"); } }
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); @Rule public final FragmentTestRule<MainActivity, UserFragment> fragmentRule = new FragmentTestRule<>( MainActivity.class, new UserFragment(), createTestApplicationComponent()); @Test public void getNameMethodCalledOnCreate() { verify(fragmentRule.getFragment().userPresenter).getUserName(); } private ApplicationComponent createTestApplicationComponent() { ApplicationComponent component = mock(ApplicationComponent.class); when(component.createUserComponent(any(UserModule.class))) .thenReturn(DaggerUserFragmentTest_TestUserComponent.create()); return component; } @Singleton @Component(modules = {TestUserModule.class}) interface TestUserComponent extends UserComponent {} @Module static class TestUserModule { @Provides public UserPresenter provideUserPresenter() { return mock(UserPresenter.class); } } }
Evgeny Aseev . . — Andrei Tarashkevich , Ruslan Login . , AURA Devices.
Source: https://habr.com/ru/post/352334/
All Articles