Android
Pisanie testów interfejsu użytkownika - Android
Szukaj…
Wprowadzenie
Celem tego dokumentu jest przedstawienie celów i sposobów pisania interfejsu użytkownika Androida i testów integracyjnych. Google zapewnia espresso i UIAutomator, więc należy skupić się na tych narzędziach i ich opakowaniach, np. Appium, Spoon itp.
Składnia
- Zasób na biegu jałowym
- String getName () - Zwraca nazwę zasobu na biegu jałowym (używanego do rejestrowania i idempotency rejestracji).
- boolean isIdleNow () - Zwraca true, jeśli zasób jest obecnie w stanie bezczynności.
- void registerIdleTransitionCallback (IdlingResource.ResourceCallback callback) - Rejestruje podany IdlingResource.ResourceCallback z zasobem
Uwagi
Zasady JUnit:
Jak widać w przykładzie MockWebServer i ActivityTestRule, wszystkie one należą do kategorii reguł JUnit, które możesz sam stworzyć, które następnie należy wykonać dla każdego testu określającego jego zachowanie @ patrz: https://github.com/junit-team/junit4/ wiki / rules
Appium
Parametry
Ponieważ parametry mają pewne problemy z umieszczeniem ich tutaj, dopóki błąd dokumentacji nie zostanie rozwiązany:
Parametr | Detale |
---|---|
Aktywność klasowa Klasa | które działanie rozpocząć |
initialTouchMode | czy aktywność powinna zostać umieszczona w trybie dotykowym na starcie: https://android-developers.blogspot.de/2008/12/touch-mode.html |
launchActivity | true, jeśli działanie należy uruchomić raz dla każdej metody testowej. Zostanie uruchomiony przed pierwszą metodą Before i zakończony po ostatniej metodzie After. |
Przykład MockWebServer
W przypadku, gdy twoje działania, fragmenty i interfejs użytkownika wymagają przetwarzania w tle, dobrym rozwiązaniem jest MockWebServer, który działa lokalnie na urządzeniu z Androidem, co zapewnia zamknięte i testowalne środowisko dla twojego interfejsu użytkownika.
https://github.com/square/okhttp/tree/master/mockwebserver
Pierwszym krokiem jest uwzględnienie zależności stopnia:
testCompile 'com.squareup.okhttp3:mockwebserver:(insert latest version)'
Teraz kroki do uruchomienia i używania fałszywego serwera to:
- utwórz próbny obiekt serwera
- uruchom go pod określonym adresem i portem (zwykle localhost: numer_portu)
- kolejkować odpowiedzi na konkretne żądania
- rozpocznij test
Jest to ładnie wyjaśnione na stronie github na serwerze mockwebserver, ale w naszym przypadku chcemy czegoś ładniejszego i do wielokrotnego użytku dla wszystkich testów, a zasady JUnit będą tu dobrze się bawić:
/**
*JUnit rule that starts and stops a mock web server for test runner
*/
public class MockServerRule extends UiThreadTestRule {
private MockWebServer mServer;
public static final int MOCK_WEBSERVER_PORT = 8000;
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
startServer();
try {
base.evaluate();
} finally {
stopServer();
}
}
};
}
/**
* Returns the started web server instance
*
* @return mock server
*/
public MockWebServer server() {
return mServer;
}
public void startServer() throws IOException, NoSuchAlgorithmException {
mServer = new MockWebServer();
try {
mServer(MOCK_WEBSERVER_PORT);
} catch (IOException e) {
throw new IllegalStateException(e,"mock server start issue");
}
}
public void stopServer() {
try {
mServer.shutdown();
} catch (IOException e) {
Timber.e(e, "mock server shutdown error”);
}
}
}
Załóżmy teraz, że mamy dokładnie taką samą aktywność jak w poprzednim przykładzie, właśnie w tym przypadku, gdy naciśniemy przycisk, aplikacja pobierze coś z sieci, na przykład: https://someapi.com/name
Zwróci to ciąg tekstowy, który zostanie połączony z tekstem paska przekąskowego, np. NAME + tekst, który wpisałeś.
/**
* Testing of the snackbar activity with networking.
**/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SnackbarActivityTest{
//espresso rule which tells which activity to start
@Rule
public final ActivityTestRule<SnackbarActivity> mActivityRule =
new ActivityTestRule<>(SnackbarActivity.class, true, false);
//start mock web server
@Rule
public final MockServerRule mMockServerRule = new MockServerRule();
@Override
public void tearDown() throws Exception {
//same as previous example
}
@Override
public void setUp() throws Exception {
//same as previous example
**//IMPORTANT:** point your application to your mockwebserver endpoint e.g.
MyAppConfig.setEndpointURL("http://localhost:8000");
}
/**
*Test methods should always start with "testXYZ" and it is a good idea to
*name them after the intent what you want to test
**/
@Test
public void testSnackbarIsShown() {
//setup mockweb server
mMockServerRule.server().setDispatcher(getDispatcher());
mActivityRule.launchActivity(null);
//check is our text entry displayed and enter some text to it
String textToType="new snackbar text";
onView(withId(R.id.textEntry)).check(matches(isDisplayed()));
//we check is our snackbar showing text from mock webserver plus the one we typed
onView(withId(R.id.textEntry)).perform(typeText("JazzJackTheRabbit" + textToType));
//click the button to show the snackbar
onView(withId(R.id.shownSnackbarBtn)).perform(click());
//assert that a view with snackbar_id with text which we typed and is displayed
onView(allOf(withId(android.support.design.R.id.snackbar_text),
withText(textToType))) .check(matches(isDisplayed()));
}
/**
*creates a mock web server dispatcher with prerecorded requests and responses
**/
private Dispatcher getDispatcher() {
final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
if (request.getPath().equals("/name")){
return new MockResponse().setResponseCode(200)
.setBody("JazzJackTheRabbit");
}
throw new IllegalStateException("no mock set up for " + request.getPath());
}
};
return dispatcher;
}
Sugeruję zawinięcie programu rozsyłającego w jakiegoś Konstruktora, aby można było łatwo łączyć i dodawać nowe odpowiedzi do ekranów. na przykład
return newDispatcherBuilder()
.withSerializedJSONBody("/authenticate", Mocks.getAuthenticationResponse())
.withSerializedJSONBody("/getUserInfo", Mocks.getUserInfo())
.withSerializedJSONBody("/checkNotBot", Mocks.checkNotBot());
IdlingResource
Moc bezczynnych zasobów polega na tym, że nie trzeba czekać na przetwarzanie niektórych aplikacji (tworzenie sieci, obliczenia, animacje itp.) Za pomocą funkcji sleep()
, co powoduje niestabilność i / lub wydłuża przebieg testów. Oficjalna dokumentacja znajduje się tutaj .
Realizacja
Podczas implementacji interfejsu IdlingResource
należy wykonać trzy czynności:
-
getName()
- Zwraca nazwę twojego zasobu na biegu jałowym. -
isIdleNow()
- Sprawdza, czy Twój obiekt xyz, operacja itp. jest w tej chwili bezczynny. -
registerIdleTransitionCallback
(IdlingResource.ResourceCallback
callback) - Zapewnia wywołanie zwrotne, które należy wywołać, gdy obiekt przechodzi w stan bezczynności.
Teraz powinieneś stworzyć własną logikę i określić, kiedy aplikacja jest bezczynna, a kiedy nie, ponieważ zależy to od aplikacji. Poniżej znajdziesz prosty przykład, aby pokazać, jak to działa. Istnieją inne przykłady online, ale konkretna implementacja aplikacji prowadzi do konkretnych implementacji zasobów na biegu jałowym.
UWAGI
- Było kilka przykładów Google, w których umieścili
IdlingResources
w kodzie aplikacji. Nie rób tego. Prawdopodobnie umieścili go tam, aby pokazać, jak działają. - Utrzymanie kodu w czystości i przestrzeganie jednej zasady odpowiedzialności zależy od Ciebie!
Przykład
Powiedzmy, że masz aktywność, która robi dziwne rzeczy i zajmuje dużo czasu, aby załadować fragment, a tym samym zawieść testy Espresso, ponieważ nie można znaleźć zasobów z tego fragmentu (powinieneś zmienić sposób, w jaki tworzona jest aktywność i kiedy przyspieszyć). Ale w każdym razie, aby to uprościć, poniższy przykład pokazuje, jak powinien on wyglądać.
Nasz przykładowy zasób na biegu jałowym otrzymałby dwa obiekty:
- Tag fragmentu, który musisz znaleźć i czekać, aby dołączyć do działania.
- Obiekt FragmentManager, który służy do znajdowania fragmentu.
/**
* FragmentIdlingResource - idling resource which waits while Fragment has not been loaded.
*/
public class FragmentIdlingResource implements IdlingResource {
private final FragmentManager mFragmentManager;
private final String mTag;
//resource callback you use when your activity transitions to idle
private volatile ResourceCallback resourceCallback;
public FragmentIdlingResource(FragmentManager fragmentManager, String tag) {
mFragmentManager = fragmentManager;
mTag = tag;
}
@Override
public String getName() {
return FragmentIdlingResource.class.getName() + ":" + mTag;
}
@Override
public boolean isIdleNow() {
//simple check, if your fragment is added, then your app has became idle
boolean idle = (mFragmentManager.findFragmentByTag(mTag) != null);
if (idle) {
//IMPORTANT: make sure you call onTransitionToIdle
resourceCallback.onTransitionToIdle();
}
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
Teraz, gdy masz już napisany IdlingResource
, musisz go gdzieś użyć, prawda?
Stosowanie
Pomińmy całą konfigurację klasy testowej i zobaczmy, jak wyglądałby przypadek testowy:
@Test
public void testSomeFragmentText() {
mActivityTestRule.launchActivity(null);
//creating the idling resource
IdlingResource fragmentLoadedIdlingResource = new FragmentIdlingResource(mActivityTestRule.getActivity().getSupportFragmentManager(), SomeFragmentText.TAG);
//registering the idling resource so espresso waits for it
Espresso.registerIdlingResources(idlingResource1);
onView(withId(R.id.txtHelloWorld)).check(matches(withText(helloWorldText)));
//lets cleanup after ourselves
Espresso.unregisterIdlingResources(fragmentLoadedIdlingResource);
}
Połączenie z regułą JUnit
To nie jest trudne; możesz także zastosować zasób biegu jałowego w formie reguły testowej JUnit. Powiedzmy na przykład, że masz pakiet SDK zawierający Volley i chcesz, aby Espresso na niego czekało. Zamiast analizować każdy przypadek testowy lub stosować go w konfiguracji, możesz utworzyć regułę JUnit i po prostu napisać:
@Rule
public final SDKIdlingRule mSdkIdlingRule = new SDKIdlingRule(SDKInstanceHolder.getInstance());
Ponieważ jest to przykład, nie bierz tego za pewnik; cały kod tutaj jest wymyślony i służy wyłącznie do celów demonstracyjnych:
public class SDKIdlingRule implements TestRule {
//idling resource you wrote to check is volley idle or not
private VolleyIdlingResource mVolleyIdlingResource;
//request queue that you need from volley to give it to idling resource
private RequestQueue mRequestQueue;
//when using the rule extract the request queue from your SDK
public SDKIdlingRule(SDKClass sdkClass) {
mRequestQueue = getVolleyRequestQueue(sdkClass);
}
private RequestQueue getVolleyRequestQueue(SDKClass sdkClass) {
return sdkClass.getVolleyRequestQueue();
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
//registering idling resource
mVolleyIdlingResource = new VolleyIdlingResource(mRequestQueue);
Espresso.registerIdlingResources(mVolleyIdlingResource);
try {
base.evaluate();
} finally {
if (mVolleyIdlingResource != null) {
//deregister the resource when test finishes
Espresso.unregisterIdlingResources(mVolleyIdlingResource);
}
}
}
};
}
}