- Testing asynchronous RxJava code using Mockito
- First Mockito test
- Changing default Mockito return values
- Testing synchronous behaviour
- TestObserver and AssertJ
- Testing asynchronous code
- Testing timeouts
- Testing exceptions and retry logic
- Wrapping up
- Тестирование уровня данных в Android Room с помощью Rxjava, LiveData и сопрограмм Kotlin
- В чем смысл начинать тестирование с уровня данных?
- Что такое уровень данных?
- Типы данных в слое данных
- Демонстрация
- База данных Room и Rxjava
- Создание DAO (объекта доступа к данным):
- Тестирование в RxJava
- База данных Room и сопрограммы (корутины)
- Создание DAO (Объекта доступа к данным)
- Тестирование в Room-ktx
- Тестирование уровня данных в Android Room с помощью Rxjava, LiveData и сопрограмм Kotlin
- В чем смысл начинать тестирование с уровня данных?
- Что такое уровень данных?
- Типы данных в слое данных
- Демонстрация
- База данных Room и Rxjava
- Создание DAO (объекта доступа к данным):
- Тестирование в RxJava
- База данных Room и сопрограммы (корутины)
- Создание DAO (Объекта доступа к данным)
- Тестирование в Room-ktx
Testing asynchronous RxJava code using Mockito
Nov 22, 2016 · 8 min read
RxJava is used in many Android applications to manage background tasks, in this post will see how to test this code using Mockito and some RxJava utility classes. The example is written using RxJava 2, the same concepts are available also in RxJava 1 (in the demo project you can find both versions of the same example).
Let’s see a simple example, we need a class (created using Retrofit) to execute server calls to StackOverflow API:
Using getTopUsers method we can retrieve a list of the top users, getBadges returns a list of badges of a specific user.
We can combine these two methods in a new class using RxJava, loadUsers method retrieves a list of the top 5 StackOverflow users ( UserStats class contains user information and the badges list):
The io scheduler is used in the loadUserStats method to execute the calls in parallel. Spoiler alert: there is a little bug in this class! We’ll see how to fix it in a while.
First Mockito test
The UserService class uses dependency injection, the StackOverflowService object is a constructor parameter. So we can easily test it using Mockito:
This test is very easy, it creates the RxJava single but it doesn’t invoke the subscribe method. It’s easy but it doesn’t work! Executing the test we get a NullPointerException:
We haven’t defined any behaviour using Mockito, so all the methods return just null. Sometimes null can be a good default value but in this example we have a strange NullPointerExeption executing the test.
Changing default Mockito return values
Using Mockito we can change default value returned in a method invocation, we need to define a class MockitoConfiguration in package org.mockito.configuration:
In this class we define that, when a method returns an Observable (or a Single), the value returned is an Observable (or a Single) that emits an exception. In this way executing the previous test we get an exception that says that we need to define the behaviour of a mock (and not a NullPointerException!).
Testing synchronous behaviour
So let’s define the behaviour of the StackOverflowService object, we return a list with two users and a list with a singe badge:
And now we need to subscribe the Single, the simplest way to do it in a test is using a blocking Single:
In this way the test waits for the end of the server calls and we can verify the value emitted by the Single. In this example I am using AssertJ, a great library that allows to write test assertions in a simple and readable way.
The blockingGet method can’t be always used, for example we can’t test an Observable that doesn’t terminate using this method. Using RxJava 2 we can use a TestObserver, it can be created invoking the test method:
This is a more flexible way to write our test, it works in every situation (even when the observable we want to test doesn’t terminate). The TestObserver class contains a lot of methods that can be used to verify the behaviour of an RxJava object (more examples on how to use this class are available in this post).
In this example we must invoke awaitTerminalEvent because the method that retrieves the badges list is executed on the io scheduler. In the next paragraph we’ll see other solutions to this problem.
Now let’s modify our test to check the ids of the users. We can use assertValue method and check the ids (we are using RxJava to extract the id on every object of the list):
The parameter of the assertValue method is a Predicate, it’s executed on the emitted value and the test fails when the return value is false. Unfortunately the error message is something similar to this one:
Reading this message is not easy to understand the cause of the error.
TestObserver and AssertJ
We can improve the previous test using a small hack and AssertJ:
The hack is in the check method, it transform a block of code that can throw an exception to a Predicate that returns a boolean. If the code throws an exception the test fails:
This is an example of an AssertJ error message, it’s very accurate and the cause of the failure can be easily understood reading it:
Testing asynchronous code
The previous test works… sometimes! We are using a flatMap with a Single executed on the io scheduler so the invocations of the getBadges method are executed in parallel. For this reason this test (and the production code!) is not deterministic, it works when the first invocation of getBadges is faster than the second one.
If we want to fix this issue we can use a Rule that replaces the default scheduler with an synchronous one (more info about this solution is available here):
Using this rule in the test we fix the previous problem but we are removing the concurrency in our code; the two invocations of getBadges are no more executed in parallel.
Let’s try to remove the nondeterministic behaviour, we can add delays to simulate that the first call is slower than the second one:
This test is deterministic, it always fails because the elements in the list are not returned in the right order. But unfortunately it last two seconds because the delays are real and we need to wait them. To fix it we can use a TestScheduler, we can create another rule similar to the previous one to replace the real schedulers with a TestScheduler:
Using a TestScheduler we can easily manage time, for example we can advance time by two seconds:
And now the test is deterministic and fast! But it fails, let’s see how to fix it.
We can replace the flatMap invocation with a concatMap, using this method the server calls to retrieve badges are executed in sequence (more info on flatMap Vs concatMap are available in this post of Fernando Cejas):
Using concatMap we need to change the test code, the delays are not executed in parallel so the total time is three seconds. To fix the test we must change the parameter of the advanceTimeBy method:
If we want to execute the server calls in parallel we can use flatMap and manually sort the data using toSortedList method using a Comparator that order the user based on the reputation:
EDIT: in the reddit thread of this post Vinaybn has suggested another solution to this problem. The method concatMapEager can be used to maintain the order and execute the observables in parallel:
Testing timeouts
Using RxJava is very easy to add a timeout to a method invocation, in our example we can just add an invocation to timeout method:
We are using the timeout method without the Scheduler parameter. This method internally use the computation scheduler, this is the source code of the method:
Using the rule we defined earlier we can replace the computation scheduler with a TestScheduler. Writing a test that simulates a timeout is easy:
Testing exceptions and retry logic
A good test should verify the behaviour of the production code when an exception occurs. So let’s add other test methods to our example.
First of all let’s add an invocation to retry method:
In the test method we can simulate that first invocation of getBadges throws an exception (the second invocation returns the correct value):
This test works correctly, the getBadges method is invoked two times with the parameter 1 thanks to the retry method.
Unfortunately we can’t write a similar test method to verify what happens when an error of getTopUsers method occurs. This method is invoked just once even when there is an error, the retry method executes another subscription to the same Single returned by the getTopUsers method.
We need to define a Single that throws an exception on the first subscription and return the right value on the second subscription. Using a boolean field we can decide the behaviour of the Single:
Wrapping up
Testing asynchronous code is never easy, it’s almost impossible to manually test all the cases and it’s hard to simulate them using a JUnit test. In this post we have seen that, using RxJava and the right libraries, it’s not so difficult and that all the corner cases can be tested using a JVM test.
Источник
Тестирование уровня данных в Android Room с помощью Rxjava, LiveData и сопрограмм Kotlin
Jul 15 · 5 min read
В чем смысл начинать тестирование с уровня данных?
Выбор архитектуры, будь то MVVM, MVP, MVC или MV, по минимуму затрагивает уровень данных. Во время архитектурных миграций он остается прежним.
Уровень данных содержит минимальные зависимости, что делает его очень простым в тестировании. Его можно протестировать модульными тестами с помощью Robolectric (что экономит время)
Что такое уровень данных?
Этот уровень отвечает за предоставление данных для приложения с помощью сетевых запросов и локальной персистентности (т.е. базы данных пространств). Он формирует основу для бизнес-уровня и уровня представления.
Типы данных в слое данных
Есть два способа, с помощью которых бизнес-уровень может запрашивать данные: одноразовый запрос и поток данных.
- Однократный запрос, или ван-шот (One-shot). Вы запрашиваете данные у БД, и она возвращает значение.
- Поток (Stream). Это в основном шаблон Pub-Sub. Вместо того, чтобы запрашивать данные из базы данных, вы подписываетесь на нее. БД уведомит вас (наблюдателя) о любых изменениях в конкретных данных.
Мы разберем те с товые примеры для RxJava/RxKotlin, сопрограмм и живых данных. Вы можете сразу перейти к тому, что задействовано у вас в приложении.
Демонстрация
Рассмотрим раздел приложения для покупок, где выводится список продуктов. Вы можете увеличить/уменьшить количество продуктов, находящихся в корзине. Также отображается суммарная стоимость всех товаров в корзине в режиме реального времени.
- Список продуктов → однократная операция.
- Объем корзины в реальном времени → поток данных. Он изменяется при изменении количества товара (приращение/уменьшение).
База данных Room и Rxjava
Создание DAO (объекта доступа к данным):
- Вставка — это завершаемая ( Completable ) операция.
- Однократная операция извлечения продуктов — это однократная ( Single ) операция.
- Поток данных объема корзины — это объект наблюдения ( Observable ).
Тестирование в RxJava
RxJava test utils предоставляет метод .test() . Он создает наблюдателя TestObserver в источнике данных и подписывается на него. Таким образом, при подписке вы сразу получаете значение из источника данных, и, следовательно, можете проверить это значение.
Приведенный ниже пример проясняет это.
- Тестирование однократной операции выборки списка продуктов
(Пожалуйста, следуйте комментариям в коде)
2. Тестирование потока данных для CartAmount в реальном времени
(Пожалуйста, следуйте комментариям в коде)
База данных Room и сопрограммы (корутины)
В Room есть поддержка сопрограмм. Запросы выполняются на кастомном диспетчере. Сопрограммы известны своей последовательностью. Но единственное условие — функции должны быть типа suspend .
Создание DAO (Объекта доступа к данным)
- Однократная операция извлечения продуктов — это функция приостановки ( suspend ).
- Объем корзины (поток данных) объявляется как поток ( Flow ) из Double .
Тестирование в Room-ktx
runBlocking запускает новую сопрограмму и блокирует текущий поток, как прерываемый, вплоть до его завершения. Следовательно, все тесты должны быть инкапсулированы внутри этого блока, что гарантирует их выполнение.
Источник
Тестирование уровня данных в Android Room с помощью Rxjava, LiveData и сопрограмм Kotlin
В чем смысл начинать тестирование с уровня данных?
Выбор архитектуры, будь то MVVM, MVP, MVC или MV, по минимуму затрагивает уровень данных. Во время архитектурных миграций он остается прежним.
Уровень данных содержит минимальные зависимости, что делает его очень простым в тестировании. Его можно протестировать модульными тестами с помощью Robolectric (что экономит время)
Что такое уровень данных?
Этот уровень отвечает за предоставление данных для приложения с помощью сетевых запросов и локальной персистентности (т.е. базы данных пространств). Он формирует основу для бизнес-уровня и уровня представления.
Типы данных в слое данных
Есть два способа, с помощью которых бизнес-уровень может запрашивать данные: одноразовый запрос и поток данных.
- Однократный запрос, или ван-шот (One-shot). Вы запрашиваете данные у БД, и она возвращает значение.
- Поток (Stream). Это в основном шаблон Pub-Sub. Вместо того, чтобы запрашивать данные из базы данных, вы подписываетесь на нее. БД уведомит вас (наблюдателя) о любых изменениях в конкретных данных.
Мы разберем тестовые примеры для RxJava/RxKotlin, сопрограмм и живых данных. Вы можете сразу перейти к тому, что задействовано у вас в приложении.
Демонстрация
Рассмотрим раздел приложения для покупок, где выводится список продуктов. Вы можете увеличить/уменьшить количество продуктов, находящихся в корзине. Также отображается суммарная стоимость всех товаров в корзине в режиме реального времени.
- Список продуктов → однократная операция.
- Объем корзины в реальном времени → поток данных. Он изменяется при изменении количества товара (приращение/уменьшение).
База данных Room и Rxjava
Создание DAO (объекта доступа к данным):
- Вставка — это завершаемая ( Completable ) операция.
- Однократная операция извлечения продуктов — это однократная ( Single ) операция.
- Поток данных объема корзины — это объект наблюдения ( Observable ).
Тестирование в RxJava
RxJava test utils предоставляет метод .test() . Он создает наблюдателя TestObserver в источнике данных и подписывается на него. Таким образом, при подписке вы сразу получаете значение из источника данных, и, следовательно, можете проверить это значение.
Приведенный ниже пример проясняет это.
- Тестирование однократной операции выборки списка продуктов
(Пожалуйста, следуйте комментариям в коде)
2. Тестирование потока данных для CartAmount в реальном времени
(Пожалуйста, следуйте комментариям в коде)
База данных Room и сопрограммы (корутины)
В Room есть поддержка сопрограмм. Запросы выполняются на кастомном диспетчере. Сопрограммы известны своей последовательностью. Но единственное условие — функции должны быть типа suspend .
Создание DAO (Объекта доступа к данным)
- Однократная операция извлечения продуктов — это функция приостановки ( suspend ).
- Объем корзины (поток данных) объявляется как поток ( Flow ) из Double .
Тестирование в Room-ktx
runBlocking запускает новую сопрограмму и блокирует текущий поток, как прерываемый, вплоть до его завершения. Следовательно, все тесты должны быть инкапсулированы внутри этого блока, что гарантирует их выполнение.
Источник