10 лучших инструментов для тестирования API
10 лучших инструментальных средств тестирования интерфейсов прикладного программирования 2018 года.
Интерес к тестированию неудержимо растёт на протяжении нескольких последних лет, согласно исследованиям Google Trends. Опрос, проведенный компанией Smartbear в 2017 году среди 5000 профессионалов в области разработки программного обеспечения, показал, что более 50% опрошенных респондентов используют автоматические средства тестирования API, и ожидается рост их количества на 30% ( с 59% до 77%) в течении следующих двух лет, причем 80% участников опроса указали, что отвечают за тестирование API.
Наличие правильных процессов, инструментов и технических решений для автоматических тестирований API становится важным, как никогда ранее. И с помощью тенденции shift-left, тестирование API становится больше, чем просто решение по контролю за качеством, теперь это критически важный компонент успешной непрерывной интеграции и развёртывания программного обеспечения.
В данной статье предоставлен обзор лучших средств тестирования API, как с открытым доступом, так и коммерческих решений, из которых команды тестировщиков могут выбрать наиболее подходящие для себя.

5 лучших средств тестирования API в 2018 году
1. SoapUI
SoapUI представляет собой консольный инструмент, предназначенный для тестирования API и позволяющий пользователям легко тестировать API REST и SOAP, а также Web-сервисы.
При помощи SoapUI, пользователи могут получить полный исходный документ и встроить предпочтительный набор функций, в дополнение к перечисленным ниже:
- Создать тест легко и быстро при помощи технологий перетаскивания объектов «мышкой» (Drag and drop) и метода «указания и щелчка» (Point-and-click)
- Быстро создать пользовательский код при помощи Groovy
- Мощное тестирование на основе данных: Данные загружаются из файлов, баз данных и Excel, поэтому они могут создать симуляцию взаимодействия пользователя и API.
- Создание комплексных сценариев и поддержка асинхронного тестирования
- Повторное использование скриптов: загрузка тестов и сканирование безопасности могут повторно использоваться в случае функционального тестирования всего в несколько шагов
2. Postman
Будучи изначально плагином браузера Chrome, теперь Postman расширяет свои технические решения вместе с оригинальными версиями как для Mac, так и для Windows.
Postman является отличным выбором API тестирования для тех, кто не желает иметь дела с кодировками в интегрированной среде разработки, используя тот же язык программирования, что и разработчик.
- Легкий в использовании клиент REST
- Богатый интерфейс делает этот инструмент простым и удобным в использовании
- Может использоваться как при автоматическом, так и при исследовательском тестировании
- Работает в приложениях для Mac, Windows, Linux и Chrome
- Обладает пакетом средств интеграции, таких как поддержка форматов Swagger и RAML
- Обладает функциями Run, Test, Document и Monitoring
- Не требует изучения нового языка программирования
- Позволяет пользователям легко делиться опытом и знаниями с другими членами команды, поскольку позволяет упаковывать все запросы и ожидаемые ответы и отправлять их коллегам.
3. Katalon Studio
Katalon Studio является бесплатным инструментом автоматического тестирования, предоставляющим общую среду для создания и выполнения UI функционала, служб API/Web и тестирования мобильных платформ.
Способность комбинировать уровни UI и Business (службы API/Web) для различных операционных сред (Windows, Mac OS, Linux) расценивается как значительное преимущество Katalon Studio перед аналогичными продуктами.
Katalon Studio поддерживает запросы SOAP и RESTful с различными типами команд (GET, POST, PUT, DELETE) с параметризованными возможностями.
Основные свойства:
- Поддержка теста комбинации между верификациями UI и API.
- Поддержка тестирования запросов как SOAP, так и RESTful.
- Сотни встроенных ключей для создания тестовых заданий.
- Поддержка одной из самых мощных библиотеки проверки утверждений AssertJ для создания динамических утверждений в BDD-стиле.
- Поддержка подхода, управляемого данными.
- Может использоваться как при автоматическом, так и при исследовательском тестировании.
- Отлично подходит как для профессионалов, так и для новичков
4. Tricentis Tosca
Tricentis Tosca представляет собой платформу непрерывного тестирования для Agile и DevOps. Среди преимуществ Tricentis Tosca следует отметить:
- Поддержку многих массивов протоколов: HTTP(s) JMS, AMQP, Rabbit MQ, TIBCO EMS, SOAP, REST, IBM MQ, NET TCP
- Интеграцию в циклы Agile и DevOps
- Максимизацию многократного использования и способность к сопровождению средств автоматизации тестирования на основе использования моделей
- Тесты API могут использоваться как на мобильных, так на браузерных и пакетных приложениях и т.д.
- Достигнута автоматизация, поддерживаемая новыми технологиями
- Снижено время, необходимое на проведение регрессивного тестирования
5. Apigee
Apigee является кросс-«облачным» средством тестирования API, позволяющим пользователям измерять и тестировать производительность API, обеспечивать техническую поддержку и разработку API при помощи других редакторов, таких как Swagger.
- Данный инструмент является многошаговым и находится под управлением Javascript
- Он позволяет разрабатывать, отслеживать, выполнять разворачивание и масштабирование API
- Идентифицирует проблемы путем отслеживания трафика API, уровня ошибок и времени ответа
- Легко создает прокси API из Open API Specification и выполняет их развертку в «облаке»
- Модели разворачивания в «облаке», локального разворачивания и гибридного разворачивания работают на основе одного кода
- PCI, HIPAA, SOC2, и PII для приложений и API
- Apigee разработан специально для цифрового бизнеса и задач с интенсивной обработкой данных на под управлением мобильных платформ API и приложений, которые управляют ним.
6. JMeter
JMeter (открытое программное обеспечение) широко используется для функционального тестирования API, однако изначально он создавался для нагрузочного тестирования.
- Поддерживает воспроизведение результатов тестирования
- Автоматически работает с файлами CSV, позволяя команде быстро создавать уникальные значения параметров для тестирования API.
- Благодаря интеграции между JMeter и Jenkins, пользователи могут включать тесты API в конвейерные обработки CI
- Данный инструмент может использоваться как для статического, так и динамического тестирования производительности ресурсов
7. Rest-Assured
Rest-Assured является общедоступным предметно-ориентированным Java-языком, который делает службу тестирования REST более простой и удобной.
- Обладает целым набором встроенных функционалов, наличие которых означает, что пользователю не придется кодировать заново.
- Надежно интегрирован со автоматизированной платформой Serenity, что позволяет пользователям комбинировать тесты UI и REST в рамках одной платформы и получать изумительные результаты.
- Поддерживает синтаксические конструкции BDD Given/When/Then
- Пользователям необязательно быть экспертами в области HTTP
8. Assertible
Assertible представляет собой инструмент тестирования API, который в первую очередь акцентируется на автоматизации процессов и надежности.
- Обеспечивает автоматическое тестирование API на каждом этапе процесса интеграции и поставки программного обеспечения.
- Осуществляет поддержку текущих тестов API после внедрения приложений и интегрирует их с привычными инструментами, такими как GitHub, Slack, и Zapier.
- Поддерживает проверку подлинности ответов HTTP при помощи готовых операторов подтверждения отсутствия ошибок, таких как проверка достоверности JSON Schema и проверка целостности данных JSON Path
9. Karate DSL
Karate DSL это новый инструмент для тестирования API, который помогает разрабатывать сценарии для BDD тестов на основе API простым способом, без написания характеристик этапов. Эти характеристики создаются самим KarateDSL, а поэтому пользователи могут запустить тестирование API легко и быстро.
- Встроен на вершине Cucumber-JVM
- Способен запускать тестирование и генерировать отчеты как любой другой стандартный проект Java
- Тест может быть разработан без обязательного владения знаниями языка Java
- Тесты могут легко создаваться даже непрограммистами
- Поддерживает конфигурацию продвижения /переключения, а также многопотоковое параллельное выполнение кода
10. Ни одно решение не может совместить все инструменты
Это больно осознавать, однако, это правда!
Мы верим, что приведенный выше список представляет наилучшие решения, доступные на текущий момент, если Вы решили использовать автоматическое тестирование API. Однако, как и в случае с большинством продуктов в данной сфере, найти один инструмент, который совмещает все указанные функции, практически невозможно.
Некоторые могут посчитать, что свойств коммерческих продуктов (Postman, Tricentis Tosca,…) будет достаточно, однако цена вопроса будет служить серьезным сдерживающим фактором. Бесплатные и общедоступные решения (Rest-Assured, Karate DSL,…) являются довольно-таки приемлемыми, но требуют квалифицированных умений и много усилий для имплементации правильной платформы. Инструменты, которые удерживают баланс между ценой и другими факторами (Katalon Studio, Postman), могут иметь недостатки для некоторых типов проектов, и эти недостатки требуют пристальной оценки.

Тестирование API создало свой собственный тренд в области автоматического тестирования, и чем дальше, тем больше инструментов будет создаваться для удовлетворения растущих запросов от команд разработки программного обеспечения. Найти идеальный инструмент по-прежнему сложно, но у нас есть и хорошие новости — теперь Ваш выбор стал намного шире, чем был раньше. Тщательно обдумайте Ваши требования, взвесьте все «за» и «против» каждого решения — постарайтесь быть не слишком придирчивым на ранних стадиях и попробуйте 5 лучших кандидатов из нашего списка. Когда Вы создадите список всех «за» и «против» для этих решений, Вы получите более адекватную картину критических факторов Вашего проекта и сможете отредактировать список инструментов. Данный подход предоставит Вам хорошую возможность идентифицировать подходящий инструмент для текущего этапа и информацию о следующем инструменте, когда Ваши проекты станут более комплексными, сложными и опытными.
Мне было бы очень приятно услышать Ваши отзывы, поэтому — если Вы знаете другие инструменты, которые могут использоваться для аналогичных целей — буду признателен за Ваш вклад в исследование темы.
- Тестирование IT-систем
- API
GoogleTest Primer
GoogleTest helps you write better C++ tests.
GoogleTest is a testing framework developed by the Testing Technology team with Google’s specific requirements and constraints in mind. Whether you work on Linux, Windows, or a Mac, if you write C++ code, GoogleTest can help you. And it supports any kind of tests, not just unit tests.
So what makes a good test, and how does GoogleTest fit in? We believe:
- Tests should be independent and repeatable. It’s a pain to debug a test that succeeds or fails as a result of other tests. GoogleTest isolates the tests by running each of them on a different object. When a test fails, GoogleTest allows you to run it in isolation for quick debugging.
- Tests should be well organized and reflect the structure of the tested code. GoogleTest groups related tests into test suites that can share data and subroutines. This common pattern is easy to recognize and makes tests easy to maintain. Such consistency is especially helpful when people switch projects and start to work on a new code base.
- Tests should be portable and reusable. Google has a lot of code that is platform-neutral; its tests should also be platform-neutral. GoogleTest works on different OSes, with different compilers, with or without exceptions, so GoogleTest tests can work with a variety of configurations.
- When tests fail, they should provide as much information about the problem as possible. GoogleTest doesn’t stop at the first test failure. Instead, it only stops the current test and continues with the next. You can also set up tests that report non-fatal failures after which the current test continues. Thus, you can detect and fix multiple bugs in a single run-edit-compile cycle.
- The testing framework should liberate test writers from housekeeping chores and let them focus on the test content. GoogleTest automatically keeps track of all tests defined, and doesn’t require the user to enumerate them in order to run them.
- Tests should be fast. With GoogleTest, you can reuse shared resources across tests and pay for the set-up/tear-down only once, without making tests depend on each other.
Since GoogleTest is based on the popular xUnit architecture, you’ll feel right at home if you’ve used JUnit or PyUnit before. If not, it will take you about 10 minutes to learn the basics and get started. So let’s go!
Beware of the Nomenclature
Note: There might be some confusion arising from different definitions of the terms Test, Test Case and Test Suite, so beware of misunderstanding these.
Historically, GoogleTest started to use the term Test Case for grouping related tests, whereas current publications, including International Software Testing Qualifications Board (ISTQB) materials and various textbooks on software quality, use the term Test Suite for this.
The related term Test, as it is used in GoogleTest, corresponds to the term Test Case of ISTQB and others.
The term Test is commonly of broad enough sense, including ISTQB’s definition of Test Case, so it’s not much of a problem here. But the term Test Case as was used in Google Test is of contradictory sense and thus confusing.
GoogleTest recently started replacing the term Test Case with Test Suite. The preferred API is TestSuite. The older TestCase API is being slowly deprecated and refactored away.
So please be aware of the different definitions of the terms:
| Meaning | GoogleTest Term | ISTQB Term |
|---|---|---|
| Exercise a particular program path with specific input values and verify the results | TEST() | Test Case |
Basic Concepts
When using GoogleTest, you start by writing assertions, which are statements that check whether a condition is true. An assertion’s result can be success, nonfatal failure, or fatal failure. If a fatal failure occurs, it aborts the current function; otherwise the program continues normally.
Tests use assertions to verify the tested code’s behavior. If a test crashes or has a failed assertion, then it fails; otherwise it succeeds.
A test suite contains one or many tests. You should group your tests into test suites that reflect the structure of the tested code. When multiple tests in a test suite need to share common objects and subroutines, you can put them into a test fixture class.
A test program can contain multiple test suites.
We’ll now explain how to write a test program, starting at the individual assertion level and building up to tests and test suites.
Assertions
GoogleTest assertions are macros that resemble function calls. You test a class or function by making assertions about its behavior. When an assertion fails, GoogleTest prints the assertion’s source file and line number location, along with a failure message. You may also supply a custom failure message which will be appended to GoogleTest’s message.
The assertions come in pairs that test the same thing but have different effects on the current function. ASSERT_* versions generate fatal failures when they fail, and abort the current function. EXPECT_* versions generate nonfatal failures, which don’t abort the current function. Usually EXPECT_* are preferred, as they allow more than one failure to be reported in a test. However, you should use ASSERT_* if it doesn’t make sense to continue when the assertion in question fails.
Since a failed ASSERT_* returns from the current function immediately, possibly skipping clean-up code that comes after it, it may cause a space leak. Depending on the nature of the leak, it may or may not be worth fixing — so keep this in mind if you get a heap checker error in addition to assertion errors.
ASSERT_EQ(x.size(), y.size()) "Vectors x and y are of unequal length"; for (int i = 0; i x.size(); ++i) EXPECT_EQ(x[i], y[i]) "Vectors x and y differ at index " i; >
Anything that can be streamed to an ostream can be streamed to an assertion macro–in particular, C strings and string objects. If a wide string ( wchar_t* , TCHAR* in UNICODE mode on Windows, or std::wstring ) is streamed to an assertion, it will be translated to UTF-8 when printed.
GoogleTest provides a collection of assertions for verifying the behavior of your code in various ways. You can check Boolean conditions, compare values based on relational operators, verify string values, floating-point values, and much more. There are even assertions that enable you to verify more complex states by providing custom predicates. For the complete list of assertions provided by GoogleTest, see the Assertions Reference.
Simple Tests
To create a test:
- Use the TEST() macro to define and name a test function. These are ordinary C++ functions that don’t return a value.
- In this function, along with any valid C++ statements you want to include, use the various GoogleTest assertions to check values.
- The test’s result is determined by the assertions; if any assertion in the test fails (either fatally or non-fatally), or if the test crashes, the entire test fails. Otherwise, it succeeds.
TEST(TestSuiteName, TestName) . test body . >
TEST() arguments go from general to specific. The first argument is the name of the test suite, and the second argument is the test’s name within the test suite. Both names must be valid C++ identifiers, and they should not contain any underscores ( _ ). A test’s full name consists of its containing test suite and its individual name. Tests from different test suites can have the same individual name.
For example, let’s take a simple integer function:
int Factorial(int n); // Returns the factorial of n
A test suite for this function might look like:
// Tests factorial of 0. TEST(FactorialTest, HandlesZeroInput) EXPECT_EQ(Factorial(0), 1); > // Tests factorial of positive numbers. TEST(FactorialTest, HandlesPositiveInput) EXPECT_EQ(Factorial(1), 1); EXPECT_EQ(Factorial(2), 2); EXPECT_EQ(Factorial(3), 6); EXPECT_EQ(Factorial(8), 40320); >
GoogleTest groups the test results by test suites, so logically related tests should be in the same test suite; in other words, the first argument to their TEST() should be the same. In the above example, we have two tests, HandlesZeroInput and HandlesPositiveInput , that belong to the same test suite FactorialTest .
When naming your test suites and tests, you should follow the same convention as for naming functions and classes.
Availability: Linux, Windows, Mac.
Test Fixtures: Using the Same Data Configuration for Multiple Tests
If you find yourself writing two or more tests that operate on similar data, you can use a test fixture. This allows you to reuse the same configuration of objects for several different tests.
To create a fixture:
- Derive a class from testing::Test . Start its body with protected: , as we’ll want to access fixture members from sub-classes.
- Inside the class, declare any objects you plan to use.
- If necessary, write a default constructor or SetUp() function to prepare the objects for each test. A common mistake is to spell SetUp() as Setup() with a small u — Use override in C++11 to make sure you spelled it correctly.
- If necessary, write a destructor or TearDown() function to release any resources you allocated in SetUp() . To learn when you should use the constructor/destructor and when you should use SetUp()/TearDown() , read the FAQ.
- If needed, define subroutines for your tests to share.
When using a fixture, use TEST_F() instead of TEST() as it allows you to access objects and subroutines in the test fixture:
TEST_F(TestFixtureClassName, TestName) . test body . >
Unlike TEST() , in TEST_F() the first argument must be the name of the test fixture class. ( _F stands for “Fixture”). No test suite name is specified for this macro.
Unfortunately, the C++ macro system does not allow us to create a single macro that can handle both types of tests. Using the wrong macro causes a compiler error.
Also, you must first define a test fixture class before using it in a TEST_F() , or you’ll get the compiler error “ virtual outside class declaration ”.
For each test defined with TEST_F() , GoogleTest will create a fresh test fixture at runtime, immediately initialize it via SetUp() , run the test, clean up by calling TearDown() , and then delete the test fixture. Note that different tests in the same test suite have different test fixture objects, and GoogleTest always deletes a test fixture before it creates the next one. GoogleTest does not reuse the same test fixture for multiple tests. Any changes one test makes to the fixture do not affect other tests.
As an example, let’s write tests for a FIFO queue class named Queue , which has the following interface:
template typename E> // E is the element type. class Queue public: Queue(); void Enqueue(const E& element); E* Dequeue(); // Returns NULL if the queue is empty. size_t size() const; . >;
First, define a fixture class. By convention, you should give it the name FooTest where Foo is the class being tested.
class QueueTest : public testing::Test protected: void SetUp() override // q0_ remains empty q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); > // void TearDown() override <> Queueint> q0_; Queueint> q1_; Queueint> q2_; >;
In this case, TearDown() is not needed since we don’t have to clean up after each test, other than what’s already done by the destructor.
Now we’ll write tests using TEST_F() and this fixture.
TEST_F(QueueTest, IsEmptyInitially) EXPECT_EQ(q0_.size(), 0); > TEST_F(QueueTest, DequeueWorks) int* n = q0_.Dequeue(); EXPECT_EQ(n, nullptr); n = q1_.Dequeue(); ASSERT_NE(n, nullptr); EXPECT_EQ(*n, 1); EXPECT_EQ(q1_.size(), 0); delete n; n = q2_.Dequeue(); ASSERT_NE(n, nullptr); EXPECT_EQ(*n, 2); EXPECT_EQ(q2_.size(), 1); delete n; >
The above uses both ASSERT_* and EXPECT_* assertions. The rule of thumb is to use EXPECT_* when you want the test to continue to reveal more errors after the assertion failure, and use ASSERT_* when continuing after failure doesn’t make sense. For example, the second assertion in the Dequeue test is ASSERT_NE(n, nullptr) , as we need to dereference the pointer n later, which would lead to a segfault when n is NULL .
When these tests run, the following happens:
- GoogleTest constructs a QueueTest object (let’s call it t1 ).
- t1.SetUp() initializes t1 .
- The first test ( IsEmptyInitially ) runs on t1 .
- t1.TearDown() cleans up after the test finishes.
- t1 is destructed.
- The above steps are repeated on another QueueTest object, this time running the DequeueWorks test.
Availability: Linux, Windows, Mac.
Invoking the Tests
TEST() and TEST_F() implicitly register their tests with GoogleTest. So, unlike with many other C++ testing frameworks, you don’t have to re-list all your defined tests in order to run them.
After defining your tests, you can run them with RUN_ALL_TESTS() , which returns 0 if all the tests are successful, or 1 otherwise. Note that RUN_ALL_TESTS() runs all tests in your link unit–they can be from different test suites, or even different source files.
When invoked, the RUN_ALL_TESTS() macro:
- Saves the state of all GoogleTest flags.
- Creates a test fixture object for the first test.
- Initializes it via SetUp() .
- Runs the test on the fixture object.
- Cleans up the fixture via TearDown() .
- Deletes the fixture.
- Restores the state of all GoogleTest flags.
- Repeats the above steps for the next test, until all tests have run.
If a fatal failure happens the subsequent steps will be skipped.
IMPORTANT: You must not ignore the return value of RUN_ALL_TESTS() , or you will get a compiler error. The rationale for this design is that the automated testing service determines whether a test has passed based on its exit code, not on its stdout/stderr output; thus your main() function must return the value of RUN_ALL_TESTS() .
Also, you should call RUN_ALL_TESTS() only once. Calling it more than once conflicts with some advanced GoogleTest features (e.g., thread-safe death tests) and thus is not supported.
Availability: Linux, Windows, Mac.
Writing the main() Function
Most users should not need to write their own main function and instead link with gtest_main (as opposed to with gtest ), which defines a suitable entry point. See the end of this section for details. The remainder of this section should only apply when you need to do something custom before the tests run that cannot be expressed within the framework of fixtures and test suites.
If you write your own main function, it should return the value of RUN_ALL_TESTS() .
You can start from this boilerplate:
#include "this/package/foo.h" #include namespace my namespace project namespace // The fixture for testing class Foo. class FooTest : public testing::Test protected: // You can remove any or all of the following functions if their bodies would // be empty. FooTest() // You can do set-up work for each test here. > ~FooTest() override // You can do clean-up work that doesn't throw exceptions here. > // If the constructor and destructor are not enough for setting up // and cleaning up each test, you can define the following methods: void SetUp() override // Code here will be called immediately after the constructor (right // before each test). > void TearDown() override // Code here will be called immediately after each test (right // before the destructor). > // Class members declared here can be used by all tests in the test suite // for Foo. >; // Tests that the Foo::Bar() method does Abc. TEST_F(FooTest, MethodBarDoesAbc) const std::string input_filepath = "this/package/testdata/myinputfile.dat"; const std::string output_filepath = "this/package/testdata/myoutputfile.dat"; Foo f; EXPECT_EQ(f.Bar(input_filepath, output_filepath), 0); > // Tests that Foo does Xyz. TEST_F(FooTest, DoesXyz) // Exercises the Xyz feature of Foo. > > // namespace > // namespace project > // namespace my int main(int argc, char **argv) testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); >
The testing::InitGoogleTest() function parses the command line for GoogleTest flags, and removes all recognized flags. This allows the user to control a test program’s behavior via various flags, which we’ll cover in the AdvancedGuide. You must call this function before calling RUN_ALL_TESTS() , or the flags won’t be properly initialized.
On Windows, InitGoogleTest() also works with wide strings, so it can be used in programs compiled in UNICODE mode as well.
But maybe you think that writing all those main functions is too much work? We agree with you completely, and that’s why Google Test provides a basic implementation of main(). If it fits your needs, then just link your test with the gtest_main library and you are good to go.
NOTE: ParseGUnitFlags() is deprecated in favor of InitGoogleTest() .
Known Limitations
- Google Test is designed to be thread-safe. The implementation is thread-safe on systems where the pthreads library is available. It is currently unsafe to use Google Test assertions from two threads concurrently on other systems (e.g. Windows). In most tests this is not an issue as usually the assertions are done in the main thread. If you want to help, you can volunteer to implement the necessary synchronization primitives in gtest-port.h for your platform.
Книга «Как тестируют в Google» — бесплатная электронная версия

Привет, Хаброжители!
В книге описано тестирование программных продуктов в Google: как устроены процессы, как организованы команды, какие техники используются, кто ответственен за качество. Принципы, на которых построено тестирование в Google, применимы в проектах и компаниях любого размера. Авторы книги сами работали над продуктами Google, создавая инструменты тестирования, настраивая процессы и занимаясь непосредственно тестированием.
Книга рассчитана на профессионалов из индустрии разработки программного обеспечения: специалистов по тестированию, программистов, менеджеров.
Отрывок. Снижение рисков
Редко удается полностью устранить риски. Мы водим машину, хоть это и опасно, но ведь нужно добираться до работы? Вообще возможность несчастного случая не означает, что он обязательно произойдет, да и, скорее всего, ничего страшного не случится. Почему? Потому что своими действиями мы снижаем возможный риск. Например, не садимся за руль в нетрезвом состоянии и не водим в условиях недостаточной видимости. Таким образом мы снижаем риски.
В разработке программного продукта самое простое — избегать рискованных областей: чем меньше кода, тем меньше риск. Но кроме использования «топора и секиры», мы можем сделать еще много чего, чтобы снизить риски:
- Мы можем проработать пользовательские истории вокруг наиболее рискованных возможностей, определить самые безопасные пути и показать их разработчикам, чтобы те ввели в приложение больше ограничений.
- Мы можем написать регрессионные тест-кейсы, чтобы убедиться, что мы отловим повторные сбои.
- Мы можем написать и запустить тесты, подтверждающие необходимость добавить механизм восстановления и отката.
- Мы можем добавить средства контроля и сторожевой код для оперативного обнаружения сбоев.
- Мы можем добавить инструменты, которые будут отслеживать изменения в поведении продукта в его разных версиях. Мы получим сигнал, если возникнет регрессионный баг.
В некоторых проектах именно тестировщиков спрашивают о готовности продукта к выпуску. Хорошему тестировщику достаточно бросить взгляд на тепловую карту, чтобы определить, стоит еще подержать продукт в духовке или пора подавать его на стол. Если речь о запуске экспериментального Google Labs, то наличие красных зон риска не так существенно, если они не относятся к безопасности, конечно. А если это выпуск новой версии Gmail, тогда даже желтые зоны представляют серьезную опасность. Такая простая цветовая градация понятна всем, даже топ-менеджерам.
Опасения по поводу рисков со временем спадают, а большой объем успешно проведенного тестирования — это хороший признак того, что риски на приемлемом уровне. Здесь мы выигрываем от того, что связываем тест-кейсы с отдельными возможностями продукта, а затем и с атрибутами и компонентами в таблице рисков. Для этого дела идеально подходит «ACC — анализ», и вот почему мы создали этот инструмент именно таким.
Тест-план за десять минут по рецепту Джеймса Уиттакера
Любая задача в разработке ПО, которую можно решить за десять минут, считается простой или не заслуживающей внимания. Предположим, что мы верим в это, — тогда что мы можем сказать о планирования тестирования? Конечно же, то, что оно занимает более десяти минут. Когда я работал директором по тестированию в Google, я руководил несколькими командами, которые создавали огромное количество тест-планов. Ответы на вопрос о том, сколько времени займет его составление, могли быть такими: «завтра», «к концу недели» и лишь пару раз — «к концу дня» (если задача озвучивалась рано утром). О’кей, примем к сведению, что составление тест-плана занимает некоторое количество часов, а то и дней.
Стоит ли такая работа усилий — это уже совсем другая история. Я вижу десятки тест-планов, которые пишут мои команды, и каждый раз это мертворожденные документы — они создаются, рецензируются, обновляются один или два раза (если повезет), а потом уверенно откладываются в долгий ящик, как только проект начинает идти не так, как это было предусмотрено. Возникает вопрос: если план не стоит того, чтобы его обновлять, стоило ли
его создавать?
Иногда тест-план нежизнеспособен потому, что содержит слишком много или, наоборот, слишком мало подробностей. Или он способствовал началу работы, а вот процессу — уже нет. И снова вопрос знатокам: стоило ли создавать документ с ограниченной или постоянно уменьшающейся ценностью?
Некоторые тест-планы содержат настолько очевидную информацию, что ее и документировать-то не стоило. Мы просто зря тратим время. Давайте посмотрим правде в глаза: у нас проблема с тест-планами.
Чтобы справиться с этим, я придумал для своей команды простое задание: написать тест-план за десять минут. Если уж он и имеет какую-то ценность, то давайте доберемся до нее как можно скорее.
Когда у вас есть всего десять минут для решения задачи, каждая секунда становится значимой. В этом моя основная идея: ограничение во времени заставляет отсекать при планировании всю шелуху и концентрироваться только на важных моментах. Делайте только то, что абсолютно необходимо, оставьте подробности исполнителям тестов. Я хотел покончить с порочной
практикой написания нежизнеспособных тест-планов, и это упражнение показалось мне верным.
Однако я ничего этого я не говорил участникам эксперимента. Я просто сказал: «Вот приложение, составьте тест-план не более чем за десять минут». Имейте в виду, что эти люди получали зарплату за то, что они выполняли мои задачи. И все же я предполагал, что они испытывали ко мне определенное уважение, а следовательно, знали, что я не поручу им невыполнимую задачу.
Они могли потратить некоторое время на знакомство с приложением, но, так как речь шла о приложениях, которые они используют каждую неделю (Google Docs, App Engine, Talk Video и т. д.), я дал им совсем немного времени на это.
Во всех случаях команды изобретали методы, схожие с методами ACC-анализа. Они оформляли решения в форме таблиц и списков, не используя большие объемы текста. То есть предложениям — да, абзацам текста — нет. Они не тратили время на форматирование текста, не вдавались в излишние объяснения. У всех тест-планов было одно общее — команды документировали возможности. Они признали, что это было лучшим решением, куда потратить весьма ограниченное время.
О’кей, ни одна команда не завершила тест-план вовремя. Тем не менее они успели за десять минут пройтись по атрибутам и компонентам и начали вычленять возможности исследуемого продукта. К концу дополнительных двадцати минут большинство моих подопытных записали довольно большой набор возможностей, который мог бы служить отличной отправной точкой
при создании тест-кейсов и пользовательских историй.
Мне кажется, что эксперимент удался. Я выделил им десять минут, хотя ориентировался на час. В итоге за полчаса было выполнено 80% работы. Разве этого недостаточно? Мы точно знаем, что не будем тестировать все, ну и зачем нам все документировать? Мы отлично знаем, что в ходе тестирования многие вещи (графики, требования, архитектура) будут изменяться. Настаивать на скрупулезной точности планирования, когда завершенность вовсе не требуется, не имеет смысла.
Восемьдесят процентов работы выполнено за тридцать минут или даже меньше. Вот это я называю десятиминутным тест-планом!
Напоследок о рисках
Google Test Analytics берет за основу описанные выше критерии оценки рисков («очень редко», «редко», «иногда», «часто»). Мы специально не хотим превращать анализ рисков в сложную задачу, иначе она не будет выполнена. Нас не интересуют точные математические подробности, потому что цифры мало что значат. Достаточно знать, что «А» рискованнее «Б», не обращая внимания на точное значение рисков. Простое знание, какая возможность рискованнее другой, позволит тест-менеджеру более эффективно распределять работу тестировщиков. А такие люди, как Патрик Коупленд, смогут легко решать, сколько тестировщиков нужно назначить в каждую команду разработки. Понимание рисков приносит пользу на уровне всей компании.
Анализ рисков — это самостоятельная научная область, уважаемая во многих отраслях. Мы используем упрощенную версию методологии, но это не мешает нам интересоваться новыми исследованиями, чтобы улучшить свой подход к тестированию. Если вы хотите узнать больше об анализе рисков, то начните со статьи «Управление рисками» в Википедии.
GTA помогает обозначить риски, а тестирование помогает их снизить. Тестировщик служит посредником в этом процессе. Он может выполнить внутренние тесты по некоторым наиболее рискованным направлениям или поставить задачу разработчикам и разработчикам в тестировании, чтобы они добавили регрессионные тесты. В его арсенале есть и другие инструменты: исследовательское тестирование, привлечение внутренних и бета-пользователей и силы внешнего сообщества.
В ответственности тестировщика знать все подверженные рискам области. Он должен стараться снизить риски любыми способами, которые ему подвластны. Вот несколько рекомендаций, которые мы считаем полезными в борьбе с рисками.
- Для самых рискованных возможностей и пар «атрибут/компонент», отмеченных красным, напишите набор пользовательских историй, сценариев использования или руководство по тестированию. В Google ответственность за наиболее рискованные возможности лежит на тестировщике. Он может координировать свою работу с коллегами, использовать разные инструменты, но личная ответственность все равно на нем.
- Внимательно изучите все то, что делалось по тестированию разработчиками и разработчиками в тестировании до вас. Как результаты повлияли на риски, выявленные с помощью GTA? Хорошо ли это тестирование было организовано с точки зрения управления рисками? Стоит ли добавить новые тесты? Тестировщику может понадобиться дописать эти тесты самому или обратиться к разработчикам. В конечном счете важно, чтобы тесты были написаны, а не кто именно их напишет.
- Проанализируйте баги, обнаруженные у каждой пары атрибут/компонент высокого риска, и убедитесь в том, что соответствующие регрессионные тесты написаны для каждого из них. Баги имеют свойство возвращаться при изменении кода.
- Будьте внимательнее к областям высокого риска — поинтересуйтесь механизмами восстановления и отката. Учтите возможное негативное влияние на пользователя, когда он столкнется с наихудшим сценарием. Обсудите такие ситуации с другими инженерами, проверьте реалистичность этих сценариев. К тестировщику, который часто кричит: «Волк!», вскоре перестанут прислушиваться. Громкие предупреждения о вероятных опасностях допустимы только в отношении сценариев с высоким риском, которые к тому же признаны реалистичными и уже были покрыты тестами.
- Вовлекайте в работу как можно больше людей, заинтересованных в успешности проекта. Внутренних пользователей следует тормошить на тему обратной связи, иначе они будут просто использовать систему, игнорируя те или иные ошибки. Просите их проводить конкретные эксперименты, задавайте им вопросы типа «А как это работает на вашей машине?» или «Как бы вы использовали такую фичу?». Сотрудники Google много участвуют в тестировании, и их нужно активно направлять именно тестировать, а не просто пользоваться продуктами.
- Если ни один из механизмов не работает, а подверженный риску компонент так и недотестирован, да еще и постоянно падает, постарайтесь добиться удаления элемента. Поздравляем! Вам выпал шанс объяснить руководству концепцию анализа рисков и подчеркнуть важность тестировщиков на проекте.
Пользовательские сценарии
Пользовательские истории описывают реальные или смоделированные способы, которыми пользователи используют приложение. Они описывают, чего хотят пользователи, смотрят на продукт их глазами, не учитывая архитектуру приложения и детали реализации.
Истории могут быть связаны с возможностями, но лишь поверхностно, поскольку все-таки подчинены действиям пользователя. Пользователю что-то нужно, а история описывает, как он использует приложение, чтобы это получить. Истории намеренно описаны в общем виде, без конкретных шагов, без жестко заданных входных данных. Только то, что будет делать пользователь, и как это воспроизвести во время тестирования приложения.
Создавая пользовательскую историю, мы смотрим на продукт только через пользовательский интерфейс, мы не включаем в описание технические подробности. Тогда тестировщик будет каждый раз проходить этот путь по-разному, как и разные пользователи по-разному решают одну и ту же задачу в нашем приложении — вот в чем главная идея!
Главное в пользовательских историях — ценность продукта для пользователя. Это не тест-кейсы с их определенными вводными данными и ожидаемыми результатами. Хорошая практика — создавать отдельные учетные записи. Мы в Google часто создаем помногу тестовых учетных записей для пользователей, описанных в историях. Старые аккаунты могут быть полезны по-другому: при тестировании Google Documents мы выявили самые интересные баги как раз для старых учетных записей — при загрузке в новой версии документов, созданных в предыдущих версиях.
Мы стараемся, чтобы тестировщики, исполняя такие сценарии, менялись. Чем больше разных способов прохождения — тем лучше.
Мы не будем слишком придираться к возможностям с низкими рисками. Мы можем решить, что писать тест-кейсы для этих областей — слишком затратное занятие. Вместо этого мы можем ограничиться исследовательским тестированием или оставить на откуп краудсорс-тестированию. Чтобы управлять работой тестировщиков из внешнего сообщества, мы часто пользуемся концепцией туров — это высокоуровневые инструкции для исследовательского тестирования. Проще говоря, такой подход дает вашему запросу нужную конкретику. Например, попросив сообщество: «Проведите FedEx-тур для такого-то набора возможностей», — мы получим намного лучший результат, чем просто отдав приложение и понадеявшись на лучшее. Мы сразу определяем фичи, которые нужно протестировать, и даем инструкции, как это делать.
Краудсорсинг
Краудсорсинг— это новое явление в тестировании. Если тестировщиков не хватает, а их ресурсы ограничены, то краудсорс-тестирование спешит на помощь! Пользователей с разными наборами устройств и программных конфигураций намного больше, чем тестировщиков. О таком количестве тестовых окружений нам остается только мечтать. Наверняка ведь найдутся желающие помочь нам?
Представим, что есть группа опытных пользователей, которые разбираются в тестировании и согласились нам помочь за разумную плату. Все, что им нужно, — это доступ к среде, где они могут работать с приложением, и отлаженный механизм предоставления обратной связи и баг-репортов. Для таких проектов, как наш опенсорсный Chromium, тестирование при помощи большой группы людей подходит идеально. Однако для проектов, открытых только внутри компании, это более проблематично. Нужно отбирать тестировщиков, пользующихся особым доверием.
Еще одна ключевая ценность краудсорсинга (кроме множества конфигураций) — это более широкий взгляд на приложение. Вместо того чтобы один тестировщик имитировал действия тысячи пользователей, у нас есть тысяча пользователей, работающих как тестировщики. Есть ли лучший способ найти сценарии, приводящие приложение к сбою, чем сразу выдать эти сценарии пользователям и получить обратную связь? Разнообразие и масштаб — вот в чем ценность краудсорсинга.
Людей, желающих протестировать программный продукт, в избытке, и доступны они круглосуточно. Допустим, дано: топ-1000 сайтов, задача: протестировать их в последней версии Chrome, тогда решение: 1 тестировщик = 1000 итераций или 20 тестировщиков = 50 итераций. Математика на стороне краудсорсинга.
Главный недостаток тестирования сообществом в том, что им нужно время, чтобы разобраться с приложением и понять, с какой стороны лучше подойти к тестированию. Большая часть этого времени теряется впустую изза количества людей, но мы придумали, как с этим справляться. Для Chrome, например, мы написали туры, и внешние тестировщики следовали им при исследовательском тестировании и при выполнении пользовательских сценариев (примеры есть в приложении Б «Туры тестов для Chrome»). Туры сразу направляли тестировщиков к нужным частям приложения и давали необходимые инструкции. Фокус в том, чтобы сделать разные наборы туров и распределить их между участниками. Так мы избежали варианта «принеси то, не знаю что» и получили именно то, о чем просили.
Краудсорс-тестирование — это следующий этап развития стандартных каналов Google: канареечного канала, канала разработки, тестового канала и канала внутреннего продукта. Это наш способ привлечения ранних пользователей и людей, которым просто нравится искать баги и сообщать о них. Мы уже попробовали набирать тестировщиков внутри компании среди наших коллег, которые любят работать со свежим продуктом, подключать к командам людей из компаний поставщиков, пользоваться услугами коммерческих компаний краудсорсинга (например, uTest). Мы даже запустили программу поощрения лучших искателей багов.
Итак, сила ACC-анализа в том, что мы получаем список возможностей продукта, который можно упорядочить по риску и закрепить за разными исполнителями. Тестировщики, работающие над одним проектом, могут получить разные наборы возможностей для проверки. Внутренние пользователи, «двадцатипроцентные» участники, тестировщики-подрядчики, тестировщики из сообщества, разработчики, разработчики в тестировании — все получат свои списки возможностей, и, к радости тестировщика, важные области будут покрыты с меньшим перекрытием, чем если бы мы просто раздали приложение для тестирования всем желающим.
Работа тестировщика, в отличие от работы разработчика в тестировании, с впуском продукта не заканчивается.
Тестирование Android приложений
Тестирование — одна из важнейших частей разработки качественных программных продуктов. Сегодня мы поговорим о некоторых методологиях и библиотеках, разработанных и используемых нашей командой для написания тестов Android приложений.

Начнем с самых базовых вещей, потому более опытные разработчики могут перейти сразу к разделу об инструментах для UI тестирования. Для тех, кому хочется узнать или освежить базовые вещи — приятного чтения.
Создание первого теста
Создадим небольшой компонент, который и будем тестировать. Он парсит файл с JSON объектом, содержащим имя, и возвращает полученную строку:
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; >>
Тут и в дальнейшем я буду приводить сокращенную версию кода. Полную версию можно посмотреть в репозитории. К каждому сниппету будет приложена ссылка на полный код.
Теперь напишем первый JUnit тест. JUnit — это Java библиотека для написания тестов. Для того, чтобы JUnit знал, что метод является тестом, нужно добавить к нему аннотацию @Test . JUnit содержит в себе класс Assert , который позволяет сравнивать фактические значения с ожидаемыми и выводит ошибку, если значения не совпадают. Этот тест будет тестировать корректность нашего компонента, а именно чтения файла, парсинга JSON и получения верного поля:
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(""); 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(""); writer.close(); String name = nameRepository.getName(); Assert.assertNotEquals(name, "Mia"); FILE.delete(); > >
Библиотеки для написания тестов
Тесты — это тоже код, который надо поддерживать. Более того, код тестов должен быть прост для понимания, чтобы его можно было верифицировать в уме. Потому есть смысл инвестировать в упрощение кода тестов, избавление от дублирования и повышение читабельности. Посмотрим на широко используемые библиотеки, которые помогут нам в этом деле.
Чтобы не дублировать код подготовки в каждом тесте, существуют аннотации @Before и @After . Методы, помеченные аннотацией @Before , будут выполняться перед каждым тестом, а помеченные аннотацией @After — после каждого теста. Также есть аннотации @BeforeClass и @AfterClass , которые выполняются соответственно перед и после всех тестов в классе. Давайте переделаем наш тест, используя такие методы:
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(""); 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"); >>
Мы смогли убрать дублирование кода настройки каждого теста. Однако, много разных классов с тестами могут потребовать создания файла, и это дублирование тоже хотелось бы убрать. Для этого есть библиотека тестовых правил (TestRule). Тестовое правило выполняет функцию схожую с @Before и @After . В методе apply() этого класса мы можем выполнить нужные нам действия до и после выполнения каждого или всех тестов. Помимо уменьшения дублирования кода, преимущество такого метода заключается еще и в том, что код выносится из класса тестов, что уменьшает количество кода в тесте и облегчает его чтение. Напишем правило для создания файла:
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(); >> >; > >
Используем это правило в нашем тесте. Для того, чтобы действия TestRule исполнялись для каждого теста, нужно пометить TestRule аннотацией @Rule .
public class NameRepositoryTest < static final File FILE = new File("test_file"); @Rule public final CreateFileRule fileRule = new CreateFileRule(FILE, ""); NameRepository nameRepository = new NameRepository(new FileReader(FILE)); @Test public void getName_isSasha() throws Exception < String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); >. >
Если правило отметить аннотацией @ClassRule , то действия будут вызываться не перед каждым тестом, а один раз перед всеми тестами в классе, аналогично аннотациям @BeforeClass и @AfterClass .
Когда в тестах используется несколько TestRule , может понадобиться, чтобы они запускались в определенном порядке, для этого существует RuleChain с помощью которого можно определить порядок запуска наших TestRule . Создадим правило, которое должно создать папку до того, как будет создан файл:
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(); >> >; > >
С этим правилом класс с тестом будет выглядеть следующим образом:
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, "")); @Test public void getName_isSasha() throws Exception < String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); >. >
Теперь в каждом тесте директория будет создаваться перед созданием файла и удаляться после удаления файла.
Google Truth — это библиотека для улучшения читабельности кода тестов. Содержит методы assert (аналогично JUnit Assert), но более читабельные для человека, а также включает гораздо больше вариантов для проверки параметров. Так выглядит предыдущий тест с использование Truth:
@Test public void getName_isSasha() throws Exception < String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); >@Test public void getName_notMia() throws Exception
Видно, что код читается почти как текст на разговорном английском языке.
Наш компонент делает две разных работы: читает файл и парсит его. Чтобы придерживаться принципа единственной ответственности, давайте выделим логику чтения файла в отдельный компонент:
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()); > >
Сейчас мы хотим тестировать именно NameRepository , а фактически тестируем и чтение файла в FileReader . Чтобы этого избежать и тем самым повысить изоляцию, надежность и скорость выполнения теста, мы можем заменить реальный FileReader на его мок.
Mockito — библиотека для для создания заглушек (моков) вместо реальных объектов для использования их в тестах. Некоторые действия, которые можно выполнять с помощью Mockito:
создавать заглушки для классов и интерфейсов;
проверять вызовы метода и значения передаваемые этому методу;
подключение к реальному объекту «шпиона» spy для контроля вызова методов.
Создадим мок FileReader и настроим его так, чтобы метод readFile() возвращал нужную нам строку:
public class NameRepositoryTest < FileReader fileReader = mock(FileReader.class); NameRepository nameRepository = new NameRepository(fileReader); @Before public void setUp() throws IOException < when(fileReader.readFile()).thenReturn(""); > @Test public void getName_isSasha() throws Exception < String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); >>
Теперь не происходит никакого чтения файла. Вместо этого, мок отдает настроенное в тесте значение.
Использование моков имеет свои преимущества:
- тесты проверяют только тестируемый класс на ошибки, ошибки других классов на проверку тестируемого класса никак не влияют
- иногда более короткий и читабельный код
- есть возможность проверять вызовы метода и передаваемые значения методам мокированного объекта
- по умолчанию ненастроенные методы возвращают null, потому все используемые методы нужно настраивать явно.
- если реальный объект имеет состояние, то при каждом его предполагаемом изменении нужно перенастраивать его мок, из-за чего код тестов иногда раздувается.
Существует более простой и удобный способ создания моков — использовать специальную аннотацию @Mock :
@Mock File file;
Есть три способа инициализировать такие моки:
- Вызвать MockitoAnnotations.initMocks():
@Before public void setUp()
- Использовать MockitoJUnitRunner для запуска тестов:
@RunWith(MockitoJUnitRunner.class)
- Добавить в тест правило MockitoRule:
@Rule public final MockitoRule rule = MockitoJUnit.rule();
Второй вариант максимально декларативен и компактен, но требует использования специального раннера тестов, что не всегда удобно. Последний вариант лишен этого недостатка и более декларативен, чем использование метода initMocks() .
Пример использования MockitoJUnitRunner
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest < @Mock FileReader fileReader; NameRepository nameRepository; @Before public void setUp() throws IOException < when(fileReader.readFile()).thenReturn(""); nameRepository = new NameRepository(fileReader); > @Test public void getName_isSasha() throws Exception < String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); >>
Host Java VM vs Android Java VM
Android тесты можно поделить на два типа: те, что можно запускать на обычной Java VM, и те, что необходимо запускать на Android Java VM. Давайте посмотрим на оба типа тестов.
Тесты, запускаемые на обычной Java VM
Тесты для кода, не требующего работы компонентов Android API, для работы которых нужен Android-эмулятор или реальное устройство, можно запускать прямо на вашем компьютере и на любой Java-машине. Преимущественно это юнит-тесты бизнес-логики, которые тестируют изолированно отдельно взятый класс. Гораздо реже пишутся интеграционные тесты, так как далеко не всегда есть возможность создать реальные объекты классов, с которыми взаимодействует тестируемый класс.
Чтобы написать класс с Host Java тестами нужно, чтобы java файл имел путь $/src/test/java/. . Также с помощью @RunWith аннотации указать Runner , который отвечает за запуск тестов, корректный вызов и обработку всех методов:
@RunWith(MockitoJUnitRunner.class) public class TestClass
Использование этих тестов имеет множество преимуществ:
- не требуют запуска эмулятора или реального устройства, особенно это важно при прохождении тестов в Continuous integration, где эмулятор может работать очень медленно и нет реального устройства
- очень быстро проходят, так как для этого не нужно запускать приложение, отображать UI и т.д.
- стабильны, так как нет проблем, связанных с тем, что эмулятор может зависнуть и т.д.
с другой стороны, этими тестами:
- нельзя в полной мере протестировать взаимодействие классов с операционной системой
- в частности, нельзя протестировать нажатия на UI элементы и жесты
Для того, чтобы была возможность использовать Android API классы в Host Java тестах, существует библиотека Robolectric, которая эмулирует среду Android и дает доступ к ее основным функциям. Однако, тестирование классов Android с Roboelectric часто работает нестабильно: нужно время, пока Robolectric будет поддерживать последнее API Android, существуют проблемы с получением ресурсов и т.д. Поэтому реальные классы почти не используются, а используются их моки для юнит-тестирования.
Для запуска тестов с помощью Roboelectric нужно установить кастомный TestRunner. В нем можно настроить версию SDK (самая последняя стабильная версия — 23), обозначить основной класс Application и другие параметры для эмулированной среды Android.
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); >>
Тесты, запускаемые на Android Java VM
Для инструментальных тестов наличие устройства или эмулятора обязательно, так как мы будем тестировать нажатие кнопок, ввод текста, и другие действия.
Чтобы написать тест для Android Java VM нужно положить java файл по пути $/src/androidTest/java/. , а также с помощью @RunWith аннотации указать AndroidJUnit4 , который позволит запускать тесты на устройстве Android.
@RunWith(AndroidJUnit4.class) public class TestClass
UI тесты
Для тестирования UI используется фреймворк Espresso, который предоставляет API для тестирования пользовательского интерфейса программы. В Espresso тесты работают в бэкграунд потоке, а взаимодействие с UI элементами в потоке UI. Espresso имеет несколько основных классов для тестирования:
- Espresso — основной класс. Содержит в себе статические методы, такие как нажатия на системные кнопки (Back, Home), вызвать/спрятать клавиатуру, открыть меню, обратится к компоненту.
- ViewMatchers — позволяет найти компонент на экране в текущей иерархии.
- ViewActions — позволяет взаимодействовать с компонентом (click, longClick, doubleClick, swipe, scroll и т.д.).
- ViewAssertions — позволяет проверить состояние компонента.
Первый UI тест
Напишем простейшее Android-приложение, которое и будем тестировать:
public class MainActivity extends AppCompatActivity < @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); >>
Протестируем наше приложение. При тестировании UI прежде всего нужно запустить Activity. Для этого существует ActivityTestRule, которое запускает Activity перед каждым тестом и закрывает после:
@Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class);
Напишем простой тест, проверяющий, что элемент с id R.id.container показан на экране:
@RunWith(AndroidJUnit4.class) public class MainActivityTest < @Rule public ActivityTestRuleactivityTestRule = new ActivityTestRule<>(MainActivity.class); @Test public void checkContainerIsDisplayed() < onView(ViewMatchers.withId(R.id.container)) .check(matches(isDisplayed())); >>
Разблокировка и включение экрана
Эмулятор на слабых или загруженных машинах может работать медленно. Поэтому между запуском эмулятора и окончанием билда с установкой приложения на эмулятор может пройти достаточно времени для того, чтобы экран заблокировался от бездействия. Таким образом тест может быть запущен при заблокированном экране, что вызовет ошибку java.lang.RuntimeException: Could not launch activity within 45 seconds . Поэтому перед запуском Activity нужно разблокировать и включить экран. Раз это нужно делать в каждом UI тесте, для избежания дублирования кода создадим правило, которое будет разблокировать и включать экран перед тестом:
class UnlockScreenRule implements TestRule < ActivityTestRuleactivityRule; UnlockScreenRule(ActivityTestRule 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(); > >; > >
Напишем кастомное ActivityTestRule , которое разблокирует экран эмулятора и запустит активити перед запуском тестов:
ActivityTestRule
public class ActivityTestRule implements TestRule < private final android.support.test.rule.ActivityTestRuleactivityRule; private final RuleChain ruleChain; public ActivityTestRule(Class activityClass) < this.activityRule = new ActivityTestRule<>(activityClass, true, true); ruleChain = RuleChain .outerRule(activityRule) .around(new UnlockScreenRule(activityRule)); > public android.support.test.rule.ActivityTestRule 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); >>
Используя это правило вместо стандартного можно сильно снизить число случайных падений UI тестов в CI.
Тестирование фрагментов
Обычно верстка и логика UI приложения не кладется вся в активити, а разбивается на окна, для каждого из которых создается фрагмент. Давайте создадим простой фрагмент для вывода на экран имени с помощью 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; >>
При открытии фрагмента UI может зависнуть на некоторое время, а если используются анимации переходов между фрагментами, тест может начаться до появления фрагмента. Поэтому нужно не просто открыть фрагмент, а дождаться, когда он будет запущен. Для ожидания результата выполнения действий отлично подходит библиотека Awaitility, которая имеет очень простой и понятный синтаксис. Напишем правило, запускающее фрагмент и ожидающее его запуска с помощью этой библиотеки:
class OpenFragmentRule implements TestRule < private final ActivityTestRuleactivityRule; private final Fragment fragment; OpenFragmentRule(ActivityTestRule 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(); >>; > >
В данном случае выражение означает, что если в течении пяти секунд фрагмент не запустится, то тест не будет пройден. Нужно отметить, что как только фрагмент запустится, тест сразу же продолжит выполнение и не будет ждать все пять секунд.
Аналогично правилу, которое запускает активити, логично создать правило, которое запускает фрагмент:
public class FragmentTestRule implements TestRule < private ActivityTestRuleactivityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule(Class activityClass, F fragment) < this.fragment = fragment; this.activityRule = new ActivityTestRule<>(activityClass); ruleChain = RuleChain .outerRule(activityRule) .around(new OpenFragmentRule<>(activityRule, fragment)); > public ActivityTestRule 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); >>
Тест фрагмента с использованием этого правила будет выглядеть следующим образом:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest < @Rule public final RuleChain rules = RuleChain .outerRule(new CreateFileRule(getTestFile(), "")) .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"); >>
Асинхронная загрузка данных во фрагментах
Так как операции с диском, а именно получение имени из файла, может выполняться сравнительно долго, то следует эту операцию выполнять асинхронно. Для асинхронного получения имени из файла используем библиотеку RxJava. Можно уверенно сказать, что RxJava сейчас используется в большинстве Android приложений. Практически каждая задача, которую нужно выполнить асинхронно, выполняется с помощью RxJava, потому что это пожалуй одна из самых удобных и понятных библиотек для асинхронного выполнения кода.
Изменим наш репозиторий так, чтобы он работал асинхронно:
public class NameRepository < . public SinglegetName() < return Single.create( emitter ->< Gson gson = new Gson(); emitter.onSuccess( gson.fromJson(fileReader.readFile(), User.class).getName()); >); > >
Для тестирования RX-кода существует специальный класс TestObserver , который автоматически подпишется на Observable и мгновенно получит результат. Тест репозитория будет выглядеть следующим образом:
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest < . @Test public void getName() < TestObserverobserver = nameRepository.getName().test(); observer.assertValue("Sasha"); > >
Обновим наш фрагмент, используя новый реактивный репозиторий:
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 implements TestRule < private final ActivityTestRuleactivityRule; private final Fragment fragment; public FragmentAsyncTestRule(Class 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()); . >
Теперь тест упадет, если асинхронные действия будут обращаться к полям фрагмента после его завершения.
Юнит-тестирование Rx кода
Создадим презентер, куда мы вынесем логику подписки на возвращаемый репозиторием 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 , чтобы протестировать корректную установку таймаута. Как обычно, напишем для этого правило:
RxImmediateSchedulerRule
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; PublishSubjectnameObservable = 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()); >>
Тестирование кода, использующего Dagger 2
Для облегчения работы с графом зависимостей объектов отлично подходит паттерн Dependency Injection. Dagger 2 — это библиотека, которая поможет в реализации этого паттерна. Поэтому в большинстве наших Android приложений все компоненты предоставляются с помощью Dagger. Об использовании и преимуществах этой библиотеки можно написать отдельную статью, а тут мы рассмотрим, как тестировать приложения, её использующие.
Начнем с того, что практически всегда при использовании Dagger существует ApplicationComponent , который предоставляет все основные зависимости приложения, и инициализируется в классе приложения Application , который, в свою очередь, имеет метод для получения этого компонента.
ApplicationComponent
@Singleton @Component(modules = ) public interface ApplicationComponent
MainApplication
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 модуль, который будет предоставлять репозиторий:
UserModule
@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)
Чтобы не заменять компонент в каждом классе с тестами фрагментов, создадим для этого правило:
class TestDaggerComponentRule implements TestRule < private final ActivityTestRuleactivityRule; private final ApplicationComponent component; TestDaggerComponentRule( ActivityTestRule 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 implements TestRule < private ActivityTestRuleactivityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule( Class 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 FragmentTestRulefragmentRule = 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 = ) 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; > > >
Тесты запускаемые только для Debug приложения
Бывает, что необходимо добавить логику иди элементы 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 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(""); nameRepository = new NameRepository(fileReader); > @Test public void getName() < TestObserverobserver = 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; PublishSubjectnameObservable = 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; PublishSubjectnameObservable = 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(), "")) .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 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 = ) interface TestUserComponent extends UserComponent <> @Module static class TestUserModule < @Provides public UserPresenter provideUserPresenter() < return mock(UserPresenter.class); >> >
Благодарности
Статья написана в коллаборации с Evgeny Aseev. Он же написал значительную часть кода наших библиотек. Спасибо за ревью текста статьи и кода — Andrei Tarashkevich, Ruslan Login. Спасибо спонсору проекта, компании AURA Devices.