[JAVA] Relationship between UI test and recording, implementation method

This article is the 20th day article of Recruit Lifestyle Advent Calendar 2017.

Introduction

My name is @AHA_oretama and my first child was born the day before yesterday. I've been busy with various private events, and I've been a little late in posting, but please forgive me!

I usually recruit and do an activity called R-SET. The word SET itself is a role advocated by Google and was introduced in Google Testing Blog. I am. The excerpt of the explanation part of SET is as follows.

The SET or Software Engineer in Test is also a developer role except their focus is on testability.

In summary, it is engineer focusing on testability → SET.

R-SET is a coined word created to improve searchability and is defined as follows.

R-SET is Recruit Lifestyle's SET.

At R-SET, by increasing the testability of Recruit Lifestyle services

Is listed as a mission.

Now that we've introduced it, I'd like to talk about recording the UI test of the app.

About UI test characteristics and recording function

First of all, make sure that you are aware of the UI test mentioned here.

スクリーンショット 2017-12-20 18.57.53.png

Do you know the word test pyramid? ?? The test pyramid is a diagram showing the characteristics of the tests to be automated, and the UI tests, Integration tests, and Unit tests are arranged in order from the top. In general, UI test often refers to end-to-end test with all modules and external services connected.

This time, the one that matches this test pyramid is called the UI test.

As a characteristic of the pyramid, the size of the pyramid represents the ideal ratio of the amount of test cases for automation. In other words, it is said that it is ideal to make many Unit tests and not too many UI tests. It also has the characteristics of being unstable, taking longer to run tests, and more costly as you go up the pyramid.

And the characteristics of this UI test lead to the need for recording.

Automation has a number of benefits, one of which is cost savings. However, due to its nature, UI tests tend to be unstable tests. If you've done UI testing, you've experienced things like, sometimes the test succeeds and sometimes the test fails, even though you haven't changed the source. Isn't it? If a test fails, you will then need to identify the cause of the test failure, but it is an unstable test and will not always reappear if you run the failed test again. If you do not execute it over and over again, the same state cannot be reproduced and it will take time to investigate the cause. Another characteristic of UI tests is that they take a long time to run. The closer the failed test is to the end of the test scenario, the longer it will take to reach that state.

Did you notice? Even though the benefits of automation are cost savings, the maintenance of automated tests is costly and the benefits are lost!

But what if the failed test was recorded at this time?

For example

You will often be able to understand the cause.

In short, recording is a response to UI test instability and increased costs due to long execution times, which is necessary to effectively benefit from automation.

How to record

Now that the UI test has shown the importance of recording, let's talk about how to record. This time I will focus on apps, so I would like to introduce the WEB if there is another opportunity.

Recording on Android device

Recording on an Android device can be executed with the following ADB command.

adb shell screenrecord {filename}

You can stop it with Ctrl + C.

Here, the output file is saved on the Android device. Since filename is the file name on the Android device, it will be something like /sdcard/demo.mp4.

I think that files saved on Android devices are often saved on the local PC. You can create a file on your Android device on your local PC with the following ADB command.

adb pull {remote} {local}

According to the User Guide, the recording function is supported for Android 4.4 (API level 19) or later. ,be careful. Also note that the maximum recording time is 180 seconds (3 minutes) at the maximum.

iOS Simulator recording

Recording of iOS Simulator can be executed by the following Xcode command-line utility command.

xcrun simctl io booted recordVideo {filename}

In the case of iOS Simulator, the save destination is the local PC. iOS can also be stopped with Ctrl + C.

According to the Release Notes Please note that this recording feature is a feature from Xcode 8.2.

Implementation in Java

We will implement the function of creating a UI test using Appium and recording the test in Java. While there are many Japanese documents for Ruby in Appium, there are few Japanese documents for other languages (although the original English site is also ...), so I think it makes sense to write Appium functions in Java. I will.

Here is a concrete implementation.

It is an environment just in case, but I have confirmed it in the following environment.

Let's introduce the implementation.

public class RecordFactory {

    public static RecordFactory factory = new RecordFactory();

    public static RecordFactory getInstance() {
        return factory;
    }

    public Record createRecord(AppiumDriver driver, String fileName, String output) {
        if (driver.getPlatformName().equals("Android")) {
            return new AdbRecord(fileName, output);
        } else {
            return new iOSSimulatorRecord(fileName, output);
        }
    }
}
interface Record extends Closeable {
  void start()
}

This is a description of the above code. ʻAppiumDriver.getPlatformName ()can be used to get the execution terminal (Android or iOS). Appium has the advantage of being a cross-platform UI test tool, so here we are making it possible to use it regardless of the recording terminal by creating a Factory class that determines the running terminal and creates a class according to it. I will. Since there was an image ofCloseable` in the recording process, I added that interface, but I think that it is OK even if there is nothing in particular.

public class AdbRecord implements Record {

    private final String fileName;
    private final String outputDir;
    private Process recordProcess;
    private final String outputPath;

    AdbRecord(String fileName,String outputDir) {
        this.fileName = fileName.endsWith(".mp4") ? fileName : fileName + ".mp4";
        this.outputDir = outputDir;
        this.outputPath = outputDir + "/" + fileName;
    }

    @Override
    public void start() throws IOException {
        ProcessBuilder builder =
            new ProcessBuilder("adb", "shell", "screenrecord", "/sdcard/" + fileName);
        builder.redirectErrorStream(true);

        recordProcess = builder.start();
    }

    @Override
    public void close() throws IOException {
        if (recordProcess != null) {
            int pid = 0;
            try {
                Field field = recordProcess.getClass().getDeclaredField("pid");
                field.setAccessible(true);
                pid = field.getInt(recordProcess);
            } catch (IllegalAccessException | NoSuchFieldException e) {
                e.printStackTrace();
            }

            while(true) {
                ProcessBuilder builder = new ProcessBuilder("kill", "-2", String.valueOf(pid));
                builder.start();
                TestUtils.sleep(1000);
                if(!recordProcess.isAlive()) {
                    break;
                }
            }
        }

        File dir = new File(outputDir);
        if(!dir.exists()) {
            dir.mkdir();
        }

        //Sleep because there is a slight time lag until the video file is completed
        TestUtils.sleep(3000);

        //Copy video files on Android device locally
        execProcess("adb", "pull", "/sdcard/" + fileName, outputPath);
        //Delete video files on Android device
        execProcess("adb", "shell", "rm", "-f","/sdcard/" + fileName);

        try (InputStream stream = Files.newInputStream(Paths.get(outputPath))) {
            Allure.addAttachment(fileName, stream);
        }
    }

    private void execProcess(String... args) throws IOException {
        ProcessBuilder builder = new ProcessBuilder(args);
        Process process = builder.start();
        try {
            process.waitFor(20, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class iOSSimulatorRecord implements Record {

    private Process recordProcess;
    private final String fileName;
    private final File outputDir;
    private final String outputPath;

    iOSSimulatorRecord(String fileName, String outputDir) {
        this.fileName = fileName.endsWith(".mov") ? fileName : fileName + ".mov";
        this.outputDir = new File(outputDir);
        this.outputPath = this.outputDir.getAbsolutePath() + "/" + fileName;
    }

    @Override
    public void start() throws IOException {
        if(!outputDir.exists()) {
            outputDir.mkdir();
        }

        ProcessBuilder builder =
            new ProcessBuilder("xcrun", "simctl", "io", "booted", "recordVideo", outputPath);
        builder.redirectErrorStream(true);

        recordProcess = builder.start();
    }

    @Override
    public void close() throws IOException {

        if (recordProcess != null) {
            int pid = 0;
            try {
                Field field = recordProcess.getClass().getDeclaredField("pid");
                field.setAccessible(true);
                pid = field.getInt(recordProcess);
            } catch (IllegalAccessException | NoSuchFieldException e) {
                e.printStackTrace();
            }

            while(true) {
                ProcessBuilder builder = new ProcessBuilder("kill", "-2", String.valueOf(pid));
                builder.start();
                TestUtils.sleep(1000);
                if(!recordProcess.isAlive()) {
                    break;
                }
            }
        }
    }
}

For both Android and iOS, the recording process for each device is started using the Java Process class. In Java, the process of getting the pid is so complicated that it's complicated, but all you're doing is getting the pid. Android is different from iOS in that it has a process of recording and then saving it to the local PC.

The user can use it as follows.

        try (Record record = recordFactory.createRecord(driver, fileName, BUILD_REPORTS_RECORDS)) {
            record.start();
            // "Run UI test"
        }

In this way, you can also record in Java.

Finally

The development of test cases with Appium is still in its infancy, and I think that various things will come out in operation and development, so I would like to introduce it again as needed.

Recommended Posts

Relationship between UI test and recording, implementation method
Relationship between package and class
Difference between instance method and class method
Difference between render method and redirect_to
Difference between == operator and equals method
Relationship between ActiveRecord with_lock and cache
Difference between == operator and eqals method
Relationship between database and model (basic)
[Java] Relationship between H2DB and JDBC
[Java] Introductory structure Class definition Relationship between class and instance Method definition format
Relationship between kotlin and java access modifiers
Relationship between Eclipse m2e plugin and Maven
[Rails] Difference between create method and new + save method
CRUD and resources method ~ 3 Not a secret relationship ~
variable and method
Ethereum Transaction Sending Method Difference between send and sendAsync
About the relationship between HTTP methods, actions and CRUD
Verification of the relationship between Docker images and containers