Taking screenshots only after failed tests
Taking screenshots in functional tests of web applications is very useful functionality. It makes more sense when we do them only after test is failed and of course when we do the screenshot of the state of application when the issue occurred. Based on screenshot we can say if it is really the issue or just flaky test that have to be fixed. We can also easier investigate and indicate what is the real issue found by the test. In the article I will describe how we can build our selenium tests with use of java and junit to take screenshots only when test is failed.
The easiest way to trigger the method after test is failed is adding the rule to your test class which extends TestWatcher class. In the class you have to override the void failed(Throwable e, Description description) method. The content of the method will be executed when the test is failed. It sounds like the solutions for the problem of taking screenshot after failed test but not always. The problem is that failed(Throwable e, Description description) method will be executed after method with @After annotation so if you have such method with e.g. closing browser then the screenshot will be taken too late(or even exception will be thrown). To solve this problem we have take the screenshot before clean up and keep it when the test is failed. To make it possible we have to create the class like this shown below. In the class we have one public method takeScreenshotToSaveWhenFailed() which should be executed just after test is finished(no matter if it is failed or not) to save the last state of application from the test scenario. The screenshot will be moved to expected location when the test is failed and it will be removed when the test is passed.
package com.testcraftsmanship.model.base; import com.testcraftsmanship.model.configuration.Configuration; import com.testcraftsmanship.model.support.DriverFactory; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import static com.testcraftsmanship.model.support.TimeDateManager.getTimeStamp; public class ScreenshotRule extends TestWatcher implements DescriptionDataExtractor { private static final Logger LOGGER = LoggerFactory.getLogger(ScreenshotRule.class); private File screenshotFile; @Override protected void failed(Throwable e, Description description) { saveScreenshotAs(generateFileNameBasedOnDescription(description)); } @Override protected void succeeded(Description description) { screenshotFile.delete(); } public void takeScreenshotToSaveWhenFailed() { LOGGER.info("Taking screenshot..."); screenshotFile = ((TakesScreenshot) DriverFactory.getDriver()) .getScreenshotAs(OutputType.FILE); } private void saveScreenshotAs(String fileName) { if (screenshotFile == null) { LOGGER.warn("Test failed but there is no screenshot to save"); return; } String targetFile = Configuration.getConfiguration().getScreenshotsSaveLocation() + File.separator + fileName; if (screenshotFile.renameTo(new File(targetFile))) { LOGGER.info("Saved screenshot in: {}", targetFile); } else { LOGGER.warn("Can't save screenshot"); } } private String generateFileNameBasedOnDescription(Description description) { String className = getClassName(description); String methodName = getMethodName(description); return className + "_" + methodName + "_" + getTimeStamp() + ".png"; } }
When the ScreenshotRule class is ready, we have to add it as a rule to all our test
classes. The easiest way is to add it to the class from which all out test classes extends.
package com.testcraftsmanship.model.base; import org.junit.Rule; public abstract class BaseTest { @Rule public ScreenshotRule screenshotRule = new ScreenshotRule(); }
The last puzzle is to execute taking sceenshot in the test class after every test. It is still not the pritiest solution because you have to invoke takeScreenshotToSaveWhenFailed() method as the first method in the cleanUp() and you have to add it to all test classes(if the content of the clean up differs) in your project. By adding that you have exact number of screenshot as number of failed tests not all test cases(which can exceed hundreds) in your run.
package com.testcraftsmanship; import com.testcraftsmanship.model.base.BaseTest; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SeleniumPlaygroundLikeTest extends BaseTest { private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumPlaygroundLikeTest.class); @Before public void setUp() { LOGGER.info("Opening application"); openApplication(); } @Test public void numberOfLikesShouldIncreaseAfterClickLikeButton() { ... } @After public void cleanUp() { screenshotRule.takeScreenshotToSaveWhenFailed(); LOGGER.info("Closing application"); closeApplication(); } }