Fully embedded Spring Boot app tests

We did some nice testing of the web controller and verified that it behaves properly. But that was just another slice. At some point, it's good to test the whole thing, end-to-end. And with today's modern suite of test tools, it's totally doable.

Spring Boot doesn't always support every tool. For example, Selenium WebDriver, a popular browser automation toolkit, is not yet supported outside of servlets.

No problem! What we really need is for Spring Boot to launch our application, preferably on an unoccupied port, and get out of the way while we do some testing. So let's do just that.

We can start by crafting a new test case like this:

    @RunWith(SpringRunner.class) 
    @SpringBootTest( 
      webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 
      public class EndToEndTests { 

This preceding test class can be described as follows:

  • @RunWith(SpringRunner.class) ensures the Spring Boot annotations integrate with JUnit.
  • @SpringBootTest is the test annotation where we can activate all of Spring Boot in a controlled fashion. With webEnvironment switched from the default setting of a mocked web environment to SpringBootTest.WebEnvironment.RANDOM_PORT, a real embedded version of the app will launch on a random available port.

This configuration will spin up a copy of our application on an open port, with a full-blown autoconfiguration, and all of our CommandLineRunners will run. That means our InitDatabase class that pre-loads MongoDB will kick in.

By the way, Flapdoodle will also run an embedded MongoDB instance because we are in the test scope.

First of all, we need a handful of test objects declared as fields of our test class. These are obtained as follows:

    static ChromeDriverService service; 
    static ChromeDriver driver; 
    @LocalServerPort 
    int port; 

These attributes of EndToEndTests can be described as follows:

  • ChromeDriverService: This gives us a handle on the bridge between Selenium and the Chrome handling library
  • ChromeDriver: This is an implementation of the WebDriver interface, giving us all the operations to drive a test browser
  • @LocalServerPort: This is a Spring Boot annotation that instructs Boot to autowire the port number of the web container into port
To use ChromeDriver, not only do we need the browser Chrome downloaded and installed in its default location, we also need a separate executable: chromedriver. Assuming you have visited https://sites.google.com/a/chromium.org/chromedriver/downloads, downloaded the bundle (macOS in my case), unzipped it, and put the executable in a folder named ext, you can proceed.

With chromedriver installed in ext, we can configure it to start and stop as follows:

    @BeforeClass 
    public static void setUp() throws IOException { 
      System.setProperty("webdriver.chrome.driver", 
        "ext/chromedriver"); 
      service = createDefaultService(); 
      driver = new ChromeDriver(service); 
      Path testResults = Paths.get("build", "test-results"); 
      if (!Files.exists(testResults)) { 
        Files.createDirectory(testResults); 
      } 
    } 
 
    @AfterClass  
    public static void tearDown() { 
      service.stop();  
    } 

This setup/teardown behavior can be described as follows:

  • @BeforeClass directs JUnit to run this method before any test method inside this class runs and to only run this method once
  • Inside the setUp method, it sets the webdriver.chrome.driver property to the relative path of chromedriver
  • Next, it creates a default service
  • Then it creates a new ChromeDriver to be used by all the test methods
  • Finally, it creates a test directory to capture screenshots (as we'll soon see)
  • @AfterClass directs JUnit to run the tearDown method after ALL tests have run in this class
  • It commands ChromeDriverService to shut down. Otherwise, the server process will stay up and running

Is this starting to sound a bit convoluted? We'll explore options to simplify this later on in this chapter.

For now, let's focus on writing this test case:

    @Test 
    public void homePageShouldWork() throws IOException { 
      driver.get("http://localhost:" + port); 
 
      takeScreenshot("homePageShouldWork-1"); 
 
      assertThat(driver.getTitle()) 
        .isEqualTo("Learning Spring Boot: Spring-a-Gram"); 
 
      String pageContent = driver.getPageSource(); 
 
      assertThat(pageContent) 
        .contains("<a href="/images/bazinga.png/raw">"); 
      WebElement element = driver.findElement(
By.cssSelector("a[href*="bazinga.png"]"));
Actions actions = new Actions(driver);
actions.moveToElement(element).click().perform();
takeScreenshot("homePageShouldWork-2");
driver.navigate().back(); }

This preceding test case can be detailed as follows:

  • @Test indicates this is a JUnit test case
  • driver navigates to the home page using the injected port
  • It takes a screenshot so we can inspect things after the fact
  • We verify the title of the page is as expected
  • Next, we grab the entire page's HTML content and verify one of the links
  • Then we hunt down that link using a W3C CSS selector (there are other options as well), move to it, and click on it
  • We grab another snapshot and then click on the back button

This is a pretty basic test. It doesn't do a lot apart from verifying the home page and checking out one link. However, it demonstrates that we can automatically test the entire system. Remember, ​we have the whole system up, including a live MongoDB database (if you count an embedded one as being real). This verifies not only our own code, but our assumptions regarding what gets autoconfigured, autowired, and initialized.

As a culmination of testing nirvana, we can even grab screen snapshots to prove we were here. Or at least that our test case was here. That code is shown here:

    private void takeScreenshot(String name) throws IOException { 
     FileCopyUtils.copy( 
      driver.getScreenshotAs(OutputType.FILE), 
      new File("build/test-results/TEST-" + name + ".png")); 
    } 

Snapping a screenshot can be explained as follows:

  • driver.getScreenshotAs(OutputType.FILE) taps the TakesScreenshot subinterface to grab a snapshot of the screen and put it into a temp file
  • Spring's FileCopyUtils utility method is used to copy that temp file into the project's build/test-results folder using the input argument to give it a custom name

Taking screenshots is a key reason to use either ChromeDriver, FirefoxDriver, or SafariDriver. All of these real-world browser integrations support this feature. And thanks to that, we have the following snapshot results:

That first shot shows the whole web page. The following screenshot shows a single image after being clicked:

The screenshot of this image may look a little awkward, but remember; these images aren't real JPGs. Instead, they are strings stuffed into the filesystem.

If we run our entire suite of test cases, we can see the whole thing takes just shy of 2.5 seconds:

Impressive, huh?

How good a test suite is that? Using the IDE, we can run the same test suite but with coverage analysis turned on:

After running the same test suite but with the IDE's coverage tools enabled, we can get a read out in the source code listing, as seen in this screenshot:

That's quite handy. We can even drill into each class and see what's missing. As deft as this is, we aren't going to delve any more into test coverage. That's something best left to a more test-oriented book.

Don't let test coverage consume you. As mentioned in my other book, Python Testing Cookbook, in Chapter 9 under the Coverage isn't everything section, the test coverage should be used to identify new, reasonable scenarios that should be checked out, not gaming the system to squeeze out another percentage point or two. And coverage reports should never be used to compare one system or test regimen against another, let alone be used as a gate for release. We should all seek to increase test coverage over time as the means to increase confidence and reduce risk, not as a gate to make releases.

We just mentioned all the ceremony invested into getting Chrome to operate. Why did we do that? Because the one WebDriver implementation that requires no such effort to bring online doesn't support taking screenshots. There is also no way of knowing if the person checking out your code has the same browser installed.

If we coded everything around Chrome because we don't like Firefox but another teammate doesn't have Chrome, we've got a problem.

On one hand, if screenshots aren't important, then HtmlUnitDriver is the way to go. It comes out of the box, works as good as any other WebDriver, and doesn't require any third-party executables or drivers. But that is the penalty of going by the least common denominator.

Wouldn't it be preferable to have whatever WebDriver we can get based on whatever we have installed on our system and automatically load that into our test case? After all, Spring Boot is about reducing Java complexity when building apps.

If you sense a slow walk toward a Spring Boot-oriented solution to this craziness, you're right. In the next section, we'll explore how to autoconfigure a WebDriver based on what's available and then we'll unit test that autoconfiguration policy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.227.9