Pimp my testAUTOmation (Teil 2)

Videos der Testdurchführung aufnehmen

Mit dieser Blogreihe möchte ich zeigen, wie sich mit einfachen Mitteln wichtige Funktionen in Selenium einbauen lassen. Im ersten Teil habe ich vorgestellt, was Selenium 4 bringt und wie Screenshots eingesetzt werden können. Im zweiten Teil erstellen wir ein Video der Testdurchführung. Dabei versuche ich, die Ansätze nach ihrem Mehrwert (The Good) und ihren Herausforderungen (The Bad) zu bewerten und ggf. nützliche Hinweise (… and the Useful) zu geben.

Warum?

Aber zum Anfang stellen wir uns kurz die Frage: Warum ein Video bzw. einen Screencast der Testdurchführung aufzeichnen?

Mit einem Video haben wir eine Aufnahme des gesamten Testlaufs. Anders als mit einem Screenshot können wir so nicht nur das Testergebnis festhalten, sondern es lässt sich auch der Weg dorthin nachvollziehen.

Wie Screenshots können auch die Videos zum Debugging dienen und auf Probleme während des Testlaufes hinweisen. Darum ist es wie beim Screenshot sinnvoll, nur dann Videos zu erzeugen, wenn Probleme auftreten. Nach diesem Ansatz sollte die Video-Funktionalität um einen flexiblen und globalen Schalter erweitert werden, der sich je nach Notwendigkeit setzen lässt.

Aber die Videos können auch der Dokumentation der Testergebnisse dienen. In manchen Projekten ist eine ausführliche Dokumentation sogar verpflichtend vorgeschrieben, da hier gesetzliche oder andere Vorgaben eingehalten werden müssen.

Videos aufnehmen leicht gemacht

Da ich Selenium mit der Java-Ausprägung nutze, war mein erster Ansatz, Videos mit den „hauseigenen“ Funktionen von Java aufzunehmen bzw. ein passendes Framework zu nutzen. Meine erste Wahl fiel auf das Framework MonteCC, da sich hier mit der Hilfe von nur zwei Methoden der Bildschirm während der Testdurchführung aufnehmen ließ. In einer Methode vor dem Test wird die Videoaufnahme gestartet und in einer Methode nach dem Test wird die Aufnahme gestoppt und das Video im passenden Verzeichnis abgelegt.

private ScreenRecorder screenRecorder;
 
@BeforeEach
public void beforeTest() {
 
    GraphicsConfiguration gc = GraphicsEnvironment
            .getLocalGraphicsEnvironment()
            .getDefaultScreenDevice()
            .getDefaultConfiguration();
 
    try {
        this.screenRecorder = new ScreenRecorder(gc,
                new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI),
                new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
                        CompressorNameKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
                        DepthKey, 24, FrameRateKey, Rational.valueOf(15),
                        QualityKey, 1.0f,
                        KeyFrameIntervalKey, 15 * 60),
                new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey,"black",
                        FrameRateKey, Rational.valueOf(30)),
                null);
        this.screenRecorder.start();
 
    } catch (Exception e) {
        System.out.println("screenRecorder.start " + e.getMessage());
    }
}
 
@AfterEach
public void afterTest()  {
 
    try {
        this.screenRecorder.stop();
        List createdMovieFiles = screenRecorder.getCreatedMovieFiles();
        for (Object movie: createdMovieFiles) {
            File m = (File)movie;
            System.out.println("New movie created: " + m.getAbsolutePath());
        }
 
    } catch (IOException ioe) {
        System.out.println("screenRecorder.stop " + ioe.getMessage());
    }
}

Damit dies vor jedem Test passiert, nutzen wir die Annotations von JUnit:

@Test

Die Methode mit der Annotation @Test ist ein Testfall und wird bei der Testausführung gestartet und das Ergebnis im Testprotokoll erfasst.

@BeforeEach

Diese Methode wird vor jeder Testaufführung aufgerufen (siehe @Test).

@AfterEach

Diese Methode wird nach jeder Testaufführung aufgerufen (siehe @Test).

Wer eine komplette Aufzeichnung aller Testfälle benötigt, kann auch die Annotations @Before und @After nutzen, die vor allen bzw. nach allen Tests einer Testsuite (Testklasse) aufgerufen werden.

Große Nachteile von MonteCC sind, dass zum einen das letzte Release bereits mehr als sechs Jahre zurückliegt und zum anderen, dass die Videos im Format QuickTime vorliegen.

Als Alternative bot sich JavaCV (https://github.com/bytedeco/javacv) an. Die Bibliothek wird in GitHub fleißig gepflegt und speichert Videos im Format MPEG. Auch hier gibt es bei der Videoerzeugung zwei Methoden, die sowohl vor als auch nach dem Test aufgerufen werden können. Aber die JavaCV benötigt weitere Methoden, da parallel zum Testlauf in kurzen Abständen Screenshots erstellt und diese dann nach dem Test zu einem Video zusammenstellt werden. Eine ausführliche Anleitung findet ihr unter folgenden Link: https://cooltrickshome.blogspot.com/2016/12/create-your-own-free-screen-recorder.html.

Für den Einsatz beim automatisierten Testen habe ich folgende Änderungen vorgenommen:

/**
 *
 * https://github.com/bytedeco/javacv
 * https://cooltrickshome.blogspot.com/2016/12/create-your-own-free-screen-recorder.html
 */
public class RecordSeleniumJavaCV {
 
    // The WebDriver is a tool for writing automated tests of websites.
    FirefoxDriver driver;
 
    public static boolean videoComplete=false;
    public static String inputImageDir= "videos" + File.separator + "inputImgFolder"+File.separator;
    public static String inputImgExt="png";
    public static String outputVideoDir= "videos" + File.separator;
    public static String outputVideo;
    public static int counter=0;
    public static int imgProcessed=0;
    public static FFmpegFrameRecorder recorder=null;
    public static int videoWidth=1920;
    public static int videoHeight=1080;
    public static int videoFrameRate=3;
    public static int videoQuality=0; // 0 is the max quality
    public static int videoBitRate=9000;
    public static String videoFormat="mp4";
    public static int videoCodec=avcodec.AV_CODEC_ID_MPEG4;
    public static Thread t1=null;
    public static Thread t2=null;
    public static boolean isRegionSelected=false;
    public static int c1=0;
    public static int c2=0;
    public static int c3=0;
    public static int c4=0;
 
    /**
     * Explanation:
     * 1) videoComplete variables tells if user has stopped the recording or not.
     * 2) inputImageDir defines the input directory where screenshots will be stored which would be utilized by the video thread
     * 3) inputImgExt denotes the extension of the image taken for screenshot.
     * 4) outputVideo is the name of the recorded video file
     * 5) counter is used for numbering the screenshots when stored in input directory.
     * 6) recorder is used for starting and stopping the video recording
     * 7) videoWidth, videoFrameRate etc define output video param
     * 8) If user wants to record only a selected region then c1,c2,c3,c4 denotes the coordinate
     *
     * @return
     * @throws Exception
     */
    public static FFmpegFrameRecorder getRecorder() throws Exception
    {
        if(recorder!=null)
        {
            return recorder;
        }
        recorder = new FFmpegFrameRecorder(outputVideo,videoWidth,videoHeight);
        try
        {
            recorder.setFrameRate(videoFrameRate);
            recorder.setVideoCodec(videoCodec);
            recorder.setVideoBitrate(videoBitRate);
            recorder.setFormat(videoFormat);
            recorder.setVideoQuality(videoQuality); // maximum quality
            recorder.start();
        }
        catch(Exception e)
        {
            System.out.println("Exception while starting the recorder object "+e.getMessage());
            throw new Exception("Unable to start recorder");
        }
        return recorder;
    }
 
    /**
     * Explanation:
     * 1) This method is used to get the Recorder object.
     * 2) We create an object of FFmpegFrameRecorder named "Recorder" and then set all its video parameters.
     * 3) Lastly we start the recorder and then return the object.
     *
     * @return
     * @throws Exception
     */
    public static Robot getRobot() throws Exception
    {
        Robot r=null;
        try {
            r = new Robot();
            return r;
        } catch (AWTException e) {
            System.out.println("Issue while initiating Robot object "+e.getMessage());
            throw new Exception("Issue while initiating Robot object");
        }
    }
 
    /**
     * Explanation:
     * 1) Two threads are started in this module when user starts the recording
     * 2) First thread calls the takeScreenshot module which keeps on taking screenshot of user screen and saves them on local disk.
     * 3) Second thread calls the prepareVideo which monitors the screenshot created in step 2 and add them continuously on the video.
     *
     * @param r
     */
    public static void takeScreenshot(Robot r)
    {
        Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
        Rectangle rec=new Rectangle(size);
        if(isRegionSelected)
        {
            rec=new Rectangle(c1, c2, c3-c1, c4-c2);
        }
        while(!videoComplete)
        {
            counter++;
            BufferedImage img = r.createScreenCapture(rec);
            try {
                ImageIO.write(img, inputImgExt, new File(inputImageDir+counter+"."+inputImgExt));
            } catch (IOException e) {
                System.out.println("Got an issue while writing the screenshot to disk "+e.getMessage());
                counter--;
            }
        }
    }
 
    /**
     * Explanation:
     * 1) If user has selected a region for recording then we set the rectangle with the coordinate value of c1,c2,c3,c4. Otherwise we set the rectangle to be full screen
     * 2) Now we run a loop until videoComplete is false (remains false until user press stop recording.
     * 3) Now we capture the region and write the same to the input image directory.
     * 4) So when user starts the recording this method keeps on taking screenshot and saves them into disk.
     *
     */
    public static void prepareVideo()
    {
        File scanFolder=new File(inputImageDir);
        while(!videoComplete)
        {
            File[] inputFiles=scanFolder.listFiles();
            try {
                getRobot().delay(500);
            } catch (Exception e) {
            }
            //for(int i=0;i<scanFolder.list().length;i++)
            for(int i=0;i<inputFiles.length;i++)
            {
                //imgProcessed++;
                addImageToVideo(inputFiles[i].getAbsolutePath());
                //String imgToAdd=scanFolder.getAbsolutePath()+File.separator+imgProcessed+"."+inputImgExt;
                //addImageToVideo(imgToAdd);
                //new File(imgToAdd).delete();
                inputFiles[i].delete();
            }
        }
        File[] inputFiles=scanFolder.listFiles();
        for(int i=0;i<inputFiles.length;i++)
        {
            addImageToVideo(inputFiles[i].getAbsolutePath());
            inputFiles[i].delete();
        }
    }
 
    /**
     * Explanation:
     * 1) cvLoadImage is used to load the image passed as argument
     * 2) We call the convert method to convert the image to frame which could be used by the recorder
     * 3) We pass the frame obtained in step 2 and add the same in the recorder by calling the record method.
     *
     * @return
     */
    public static OpenCVFrameConverter.ToIplImage getFrameConverter()
    {
        OpenCVFrameConverter.ToIplImage grabberConverter = new OpenCVFrameConverter.ToIplImage();
        return grabberConverter;
    }
 
    /**
     * Explanation:
     * 1) We start a loop which will run until video complete is set true (done only when user press stop recording)
     * 2) We keep on monitoring the input  Image directory
     * 3) We traverse each file found in the input image directory and add those images to video using the addImageToVideo method. After the image has been added we delete the image
     * 4) Using the loop in step1 we keep on repeating step 2 and 3 so that each image gets added to video. We added a delay of 500ms so that this module does not picks a half created image from the takeScreenshot module
     * 5) When user press stop recording the loop gets broken. Now we finally traverse the input image directory and add the remaining images to video.
     *
     * @param imgPath
     */
    public static void addImageToVideo(String imgPath)
    {
        try {
            getRecorder().record(getFrameConverter().convert(cvLoadImage(imgPath)));
        } catch (Exception e) {
            System.out.println("Exception while adding image to video "+e.getMessage());
        }
    }
 
    /**
     * Explanation:
     * 1) We make a JFrame with the button for staring and stopping the recording. One more button is added for allowing user to record only a selected portion of screen
     * 2) If user clicks to select only certain region then we call a class CropRegion method getImage which helps in retrieving the coordinate of the region selected by user and update the same in variable c1,c2,c3,c4
     * 3) If user clicks on start recording then startRecording method is called
     * 4) If user clicks on stoprecording then stopRecording method is called
     */
    @BeforeEach
    public void beforeTest() {
 
        System.out.println("this.screenRecorder.start()");
 
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String timestamp  = dateFormat.format(new Date());
 
        outputVideo= outputVideoDir + "recording_" + timestamp + ".mp4";
 
        try {
            t1=new Thread()
            {
                public void run() {
                    try {
                        takeScreenshot(getRobot());
                    } catch (Exception e) {
                        System.out.println("Cannot make robot object, Exiting program "+e.getMessage());
                        System.exit(0);
                    }
                }
            };
            t2=new Thread()
            {
                public void run() {
                    prepareVideo();
                }
            };
            t1.start();
            t2.start();
            System.out.println("Started recording at "+new Date());
 
 
        } catch (Exception e) {
            System.out.println("screenRecorder.start " + e.getMessage());
        }
    }
 
    @AfterEach
    public void afterTest()  {
 
        System.out.println("this.screenRecorder.stop()");
 
        try {
            videoComplete=true;
            System.out.println("Stopping recording at "+new Date());
            t1.join();
            System.out.println("Screenshot thread complete");
            t2.join();
            System.out.println("Video maker thread complete");
            getRecorder().stop();
            System.out.println("Recording has been saved successfully at "+new File(outputVideo).getAbsolutePath());
 
        } catch (Exception e) {
            System.out.println("screenRecorder.stop " + e.getMessage());
        }
    }
 
    @Test
    public void testMe() {
         
        //toDo
 
    }
 
}

Gibt es einen Haken?

Somit haben wir nun mehrere Möglichkeiten, den automatischen Testlauf in einem Video festzuhalten, um bei Problemen diese nachvollziehen oder die Dokumentationspflichten erfüllen zu können.

Aber die Aufnahme während des Testdurchlaufes ist mit einem Risiko verbunden, dass der Tester / Testautomatisierer beachten sollte: Es ist ein Eingriff den normalen Testablauf, da wir ggf. die Timings der Geschwindigkeit ändern. Unser modifizierter Testlauf mit Videoaufnahme kann sich anders verhalten als ein Testlauf ohne Videoaufnahme.

Videos der Testdurchführung

The GoodThe Bad… and the Useful
• Aufnahme des gesamten Testlaufs
• Neben dem Testergebnis lässt sich auch der Weg dorthin nachvollziehen

• Es wird der Testlauf beeinflusst
• Verschiedene alte und kostenpflichtige Frameworks zur Videoaufnahme
• Codecs Probleme (nur QuicktimeView)
• JavaCV →
github.com/bytedeco/javacv




Besuche auf dieser Seite:147