Статьи

Как написать тесты для разработки под Android

Автоматизированные модульные тесты для приложения Android необходимы для его долгосрочного качества. Модульные тесты помогают протестировать одну единицу вашего кода (например, класс). Это помогает в обнаружении и выявлении ошибок или регрессий на самых ранних этапах цикла разработки. В этой статье мы увидим, как мы можем написать модульные тесты для нашего приложения для Android. В Android модульные тесты могут быть двух типов:

  • Локальные модульные тесты — которые выполняются на самой машине разработки, а не на реальной машине
  • Инструментированный юнит-тест — который работает на реальном устройстве Android.

Создание юнит-тестов с объектами JUnit и Mocking с помощью Mockito

Мы начнем с создания локальных модульных тестов для приложения Android, которое добавляет два числа.
Приложение содержит следующий вспомогательный класс

package com.testsinandroid;

import android.support.annotation.VisibleForTesting;


public class NumberAdder {

       private final MainActivity mMainActivity;
       public NumberAdder(MainActivity activity) {
            mMainActivity = activity;
        }

    public void performAddition() {

        double number1 = mMainActivity.getFirstNumber();
        double number2 = mMainActivity.getSecondNumber();

        if(!isNumberValid(number1) || !isNumberValid(number2)) {
            throw new RuntimeException("invalid numbers");
        }

        double result = number1 + number2;

        mMainActivity.setAdditionResult(result);
    }

    @VisibleForTesting
    boolean isNumberValid(double number) {
        if(number > 0) {
            return true;
        } else {
            return false;
        }
    }
}

А также содержит активность, которая использует этот класс

 package com.testsinandroid;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;


public class MainActivity extends Activity {


    EditText firstNumber;
    EditText secondNumber;
    TextView addResult;
    Button btnAdd;

    NumberAdder numberAdder = null;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        firstNumber = (EditText)findViewById(R.id.txtNumber1);
        secondNumber = (EditText)findViewById(R.id.txtNumber2);
        addResult = (TextView)findViewById(R.id.txtResult);
        btnAdd = (Button)findViewById(R.id.btnAdd);

        if(numberAdder == null) {
            numberAdder = new NumberAdder(this);
        }

        btnAdd.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                numberAdder.performAddition();
            }
        });


    }

    public double getFirstNumber() {
        return Double.parseDouble(firstNumber.getText().toString());
    }

    public double getSecondNumber() {
        return Double.parseDouble(secondNumber.getText().toString());
    }

    public void setAdditionResult(double result) {
        addResult.setText(Double.toString(result));
    }

}

План действий выглядит следующим образом

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <EditText
        android:id="@+id/txtNumber1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="2"
        android:inputType="number" >

        <requestFocus />
    </EditText>

    <EditText
        android:id="@+id/txtNumber2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/txtNumber1"
        android:ems="2"
        android:inputType="number" >

    </EditText>

    <Button
        android:id="@+id/btnAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="46dp"
        android:layout_below="@+id/txtNumber2"
        android:text="Add" />

    <TextView
        android:id="@+id/txtResult"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnAdd"
        android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>

Когда наше приложение будет готово, у нас есть два основных модуля для тестирования в нашем коде.

  • NumberAdder — это простой Java-класс, который использует MainActivity в качестве зависимости.
  • MainActivity — действие, отображающее пользовательский интерфейс.

Так как NumberAdderJunit для тестирования таких классов.
JUnit — это простая структура для написания повторяющихся модульных тестов. Когда мы тестируем один модуль (в данном случае NumberAdder), все другие классы, от которых зависит NumberAdder, могут быть отключены. Хорошей платформой для макетирования зависимостей в Java является Mockito . Чтобы добавить JUnit и Mockito в качестве тестовых зависимостей в наш проект, добавьте в build.gradle следующее

 dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'


    // Required -- JUnit 4 framework
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.10.19'


}

Чтобы написать тест Junit + Mockito для NumberAdder, создайте файл NumberAdderTest.java в папке
src / test / java / com / testsinandroid со следующим содержимым

 package com.testsinandroid;
import com.testsinandroid.MainActivity;
import com.testsinandroid.NumberAdder;

import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class NumberAdderTest {

    @Mock
    MainActivity mMockMainActivity;

    @Test
    public void testIsNumberValid() {
        //setup

        //test
        NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
        assert(numberAdder.isNumberValid(55.0));

    }

    @Test
    public void testIsNumberNotValid() {
        //setup

        //test
        NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
        assertFalse(numberAdder.isNumberValid(-55.0));

    }

    @Test
    public void testPerformAddition() {
        //setup
        when(mMockMainActivity.getFirstNumber())
                .thenReturn(10.0);
        when(mMockMainActivity.getSecondNumber())
                .thenReturn(11.0);

        //test
        NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
        numberAdder.performAddition();

        //verify
        verify(mMockMainActivity).setAdditionResult(21.0);

    }

}

Приведенный выше код создает три теста, которые аннотируются аннотацией @Test. Тест запускается с использованием MockitoJUnitRunner. Этот бегун вводит фиктивный объект для каждого поля, помеченного @Mock. MainActivity подвергается насмешкам в приведенном выше тесте. В первых двух тестах мы делаем утверждение на основе значения, возвращаемого функцией. В третьем тесте мы устанавливаем значения, которые должны возвращаться при вызове getFirstNumber и getSecondNumber. Затем мы проверяем, имеет ли метод setAdditionResult правильное значение для mMockMainActivity.

После того, как мы написали эти локальные тесты JUnit, мы можем запустить их с помощью команды gradle. Сборка будет успешной, если все тесты пройдут или не пройдут, если они не пройдут.

 ./gradlew test

Написать тесты с использованием Roboelectric

Robolectric — это инфраструктура модульного тестирования для Android. С Roboelectric вы можете запустить тестовый модуль Android на JVM на вашей рабочей станции. Это очень удобно для модульного тестирования кода Android на рабочей станции. Сборка может быть неудачной, если тесты не пройдены.

Чтобы добавить Roboelectric к своим тестовым зависимостям, обновите свои зависимости следующим образом

 dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'


    // Add Junit and mockito as testing dependencies
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.10.19'

    //For Roboelectric
    testCompile "org.robolectric:robolectric:3.0"
}

Как только мы добавили зависимость, мы можем добавить следующий тест для MainActivity.

 package com.testsinandroid;


import org.junit.Test;
import org.junit.Before;

import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import static org.junit.Assert.*;

import static org.junit.Assert.assertTrue;
import org.robolectric.RobolectricGradleTestRunner;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {

    MainActivity activity;
    EditText firstNumber;
    EditText secondNumber;
    TextView addResult;
    Button btnAdd;

    @Before
    public void setUp() {
        activity = Robolectric.setupActivity(MainActivity.class);
        firstNumber = (EditText)activity.findViewById(R.id.txtNumber1);
        secondNumber = (EditText)activity.findViewById(R.id.txtNumber2);
        addResult = (TextView)activity.findViewById(R.id.txtResult);
        btnAdd = (Button) activity.findViewById(R.id.btnAdd);
    }

    @Test
    public void testMainActivityAddition() {
        //setup 

        firstNumber.setText("12.2");
        secondNumber.setText("13.3");

        //test
        btnAdd.performClick();

        //verify
        assertEquals(25.5, Double.parseDouble(addResult.getText().toString()), 0.0);

    }
}

В приведенном выше тесте мы создаем MainActivityTest. Он запускается с помощью RobolectricGradleTestRunner.
Затем мы используем Roboelectric API ‘Robolectric.setupActivity’, который создает действие. Он также вызывает свои методы жизненного цикла, такие как «OnCreate», «onStart» и т. Д. Затем мы получаем различные элементы представления в действии и устанавливаем значения. Как только значения установлены, мы нажимаем на кнопку. Затем мы наконец проверяем, что у результата TextView есть соответствующее значение, используя ‘assertEquals’.

Как показано в приведенном выше примере, Roboelectric позволяет легко написать модульный тест для вашего кода Android.

Написание тестов Android Instrumentation

Android позволяет писать тестовые модули инструментов. Это тесты, которые работают на реальном устройстве Android. Поскольку вы тестируете на реальном устройстве, вам не нужно издеваться над классами Android. Тестирование компонентов Android, таких как активность и т. Д., Становится проще с помощью теста на интрументацию. Но проблема в том, что вам нужно реальное устройство для запуска теста. Они не могут запустить его на вашей рабочей станции.

Чтобы добавить инструментарий, обновите зависимости Gradle следующим образом.

 dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'


    // Add Junit and mockito as testing dependencies
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.10.19'

    //For Roboelectric
    testCompile "org.robolectric:robolectric:3.0"

    //For Instrumentation tests

    androidTestCompile 'com.android.support:support-annotations:25.0.1'
    androidTestCompile 'com.android.support.test:runner:0.5'
    androidTestCompile 'com.android.support.test:rules:0.5'
}

Чтобы написать тест на проверку подлинности для MainActivity, создайте файл MainActivityIntrumentationTest.java. Файл должен быть в папке
src / androidTest / java / com / testsinandroid со следующим содержимым

 package com.testsinandroid;


import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2;
import android.test.TouchUtils;
import android.test.ViewAsserts;
import android.view.View;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import static org.junit.Assert.*;
import android.test.UiThreadTest; 



public class MainActivityIntrumentationTest extends ActivityInstrumentationTestCase2<MainActivity> {
    MainActivity activity;
    EditText firstNumber;
    EditText secondNumber;
    TextView addResult;
    Button btnAdd;

  public MainActivityIntrumentationTest() {
    super(MainActivity.class);
  }


  @Override
  protected void setUp() throws Exception {
    super.setUp();

    setActivityInitialTouchMode(true);

    activity = getActivity();
    firstNumber = (EditText)activity.findViewById(R.id.txtNumber1);
    secondNumber = (EditText)activity.findViewById(R.id.txtNumber2);
    addResult = (TextView)activity.findViewById(R.id.txtResult);
    btnAdd = (Button) activity.findViewById(R.id.btnAdd);
  }

   @UiThreadTest
   public void testMainActivityAddition() {

        //setup 
        firstNumber.setText("12.2");
        secondNumber.setText("13.3");

        //test
        btnAdd.performClick();

        //verify
        assertEquals(25.5, Double.parseDouble(addResult.getText().toString()), 0.0);

   }

}

В приведенном выше коде наш тестовый класс наследуется от ActivityInstrumentationTestCase2. ActivityInstrumentationTestCase2 позволяет нам написать тест для Activity. В конструкторе этого класса мы должны пройти тестируемое действие («MainActivity» в нашем случае). Затем мы переопределяем метод ‘setUp’, в котором мы получаем объект действия и другие элементы пользовательского интерфейса действия. Затем мы пишем тест, который похож на тот, который мы написали в предыдущем разделе. Тест имеет аннотацию «@UiThreadTest». Это сделает этот тест выполненным в потоке пользовательского интерфейса, так как в этом тесте есть несколько операций пользовательского интерфейса.
Мы можем запустить тестирование после подключения устройства с помощью команды

 ./gradlew connectedCheck

Вывод

В этой статье мы увидели много способов написания модульных тестов для вашего кода Android. В зависимости от ваших потребностей в проекте, вы можете иметь один или несколько типов тестов в вашем проекте. Автоматизированные модульные тесты имеют много долгосрочных преимуществ. Их следует рассматривать и оценивать как часть ваших усилий по разработке. Описанные выше фреймворки помогают писать модульные тесты для вашего приложения для Android. Они делают большую часть тяжелой работы под капотом для вас, чтобы вы могли сосредоточиться на тесте. Так что получайте удовольствие от написания юнит-тестов в следующем Android-приложении.