Testing your custom Spring Boot autoconfiguration

If picking between several WebDriver implementations sounds hokey and unnecessarily complicated, then let's do what Spring Boot does best: autoconfigure it!

Okay, if we're going to autoconfigure something, we sure as heck want to test what we're doing. That way, we can make sure it performs as expected. To do so requires a little bit of test setup. Check it out:

    public class WebDriverAutoConfigurationTests { 
      private AnnotationConfigApplicationContext context; 
      @After 
      public void close() { 
        if (this.context != null) { 
          this.context.close(); 
        } 
      } 
 
      private void load(Class<?>[] configs, String... environment) { 
        AnnotationConfigApplicationContext applicationContext = 
          new AnnotationConfigApplicationContext(); 
        applicationContext 
          .register(WebDriverAutoConfiguration.class); 
        if (configs.length > 0) { 
          applicationContext.register(configs); 
        } 
        EnvironmentTestUtils 
          .addEnvironment(applicationContext, environment); 
        applicationContext.refresh(); 
        this.context = applicationContext; 
      }

...more coming later...
}

This preceding test case is set up as follows:

  • It starts off very different from what we've seen up until now. Instead of using various Spring Boot test annotations, this one starts with nothing. That way, we can add only the bits of Boot that we want in a very fine-grained fashion.
  • We'll use Spring's AnnotationConfigApplicationContext as the DI container of choice to programmatically register beans.
  • The @After annotation flags the close() method to run after every test case and close the application context, ensuring the next test case has a clean start.
  • load() will be invoked by each test method as part of its setup, accepting a list of Spring configuration classes as well as optional property settings, as it creates a new application context.
  • load() then registers a WebDriverAutoConfiguration class (which we haven't written yet).
  • After that, it registers any additional test configuration classes we wish.
  • It then uses Spring Boot's EnvironmentTestUtils to add any configuration property settings we need to the application context. This is a convenient way to programmatically set properties without mucking around with files or system settings.
  • It then uses the application context's refresh() function to create all the beans.
  • Lastly, it assigns the application context to the test class's context field.

In this bit of code, we programmatically build up a Spring application context from scratch. In this test class, we register our brand new WebDriverAutoConfiguration class to be at the heart of all of our tests. Then we are free to run all kinds of test cases, ensuring it acts properly. We can even register different configuration classes to override any of the autoconfiguration beans.

Now let's noodle out our first test case. What's a good place to start? What if we were to disable all the browser-based WebDriver instances (like Firefox and Chrome), and instead, expect the thing to fall back to the universal HtmlUnitDriver? Let's try it:

    @Test 
    public void fallbackToNonGuiModeWhenAllBrowsersDisabled() { 
      load(new Class[]{}, 
        "com.greglturnquist.webdriver.firefox.enabled:false", 
        "com.greglturnquist.webdriver.safari.enabled:false", 
        "com.greglturnquist.webdriver.chrome.enabled:false"); 
   
      WebDriver driver = context.getBean(WebDriver.class); 
      assertThat(ClassUtils.isAssignable(TakesScreenshot.class, 
        driver.getClass())).isFalse(); 
      assertThat(ClassUtils.isAssignable(HtmlUnitDriver.class, 
        driver.getClass())).isTrue(); 
    } 

This test case can be explained as follows:

  • @Test marks fallbackToNonGuiModeWhenAllBrowsersDisabled as a JUnit test method.
  • To start things, it uses the load() method. Since we don't have any custom overrides, we supply it with an empty array of configuration classes. We also include a slew of properties, the first one being com.greglturnquist.webdriver.firefox.enabled:false. From a design perspective, it's nice to optionally exclude certain types, so having a well-qualified property (using a domain we own) and setting them all to false sounds like a good start.
  • Now we can ask the application context to give us a WebDriver bean.
  • If it bypassed all those browser-specific ones and landed on HtmlUnitDriver, then it shouldn't support the TakesScreenshot interface. We can verify that with the AssertJ assertThat() check, using Spring's ClassUtils.isAssignable check.
  • To make it crystal clear that we're getting an HtmlUnitDriver, we can also write another check verifying that.

Since we aren't actually testing the guts of Selenium WebDriver, there is no need to examine the object anymore. We have what we want, an autoconfigured WebDriver that should operate well.

Having captured our first expected set of conditions, it's time to roll up our sleeves and get to work. We'll start by creating WebDriverAutoConfiguration.java as follows:

    @Configuration 
    @ConditionalOnClass(WebDriver.class) 
    @EnableConfigurationProperties( 
      WebDriverConfigurationProperties.class) 
    @Import({ChromeDriverFactory.class, 
      FirefoxDriverFactory.class, SafariDriverFactory.class}) 
    public class WebDriverAutoConfiguration { 
      ... 
    } 

This preceding Spring Boot autoconfiguration class can be described as follows:

  • @Configuration: This indicates that this class is a source of beans' definitions. After all, that's what autoconfiguration classes do--create beans.
  • @ConditionalOnClass(WebDriver.class): This indicates that this configuration class will only be evaluated by Spring Boot if it detects WebDriver on the classpath, a telltale sign of Selenium WebDriver being part of the project.
  • @EnableConfigurationProperties(WebDriverConfigurationProperties.class): This activates a set of properties to support what we put into our test case. We'll soon see how to easily define a set of properties that get the rich support Spring Boot provides of overriding through multiple means.
  • @Import(...​): This is used to pull in extra bean definition classes.

This class is now geared up for us to actually define some beans pursuant to creating a WebDriver instance. To get an instance, we can imagine going down a list and trying one such as Firefox. If it fails, move on to the next. If they all fail, resort to using HtmlUnitDriver.

The following class shows this perfectly:

    @Primary 
    @Bean(destroyMethod = "quit") 
    @ConditionalOnMissingBean(WebDriver.class)  
    public WebDriver webDriver( 
      FirefoxDriverFactory firefoxDriverFactory, 
      SafariDriverFactory safariDriverFactory, 
      ChromeDriverFactory chromeDriverFactory) { 
        WebDriver driver = firefoxDriverFactory.getObject(); 
 
        if (driver == null) { 
          driver = safariDriverFactory.getObject(); 
        } 
 
        if (driver == null) { 
          driver = chromeDriverFactory.getObject(); 
        } 
 
        if (driver == null) { 
          driver = new HtmlUnitDriver(); 
        } 
 
        return driver; 
      }

This WebDriver creating code can be described as follows:

  • @Primary: This indicates that this method should be given priority when someone is trying to autowire a WebDriver bean over any other method (as we'll soon see).
  • @Bean(destroyMethod = "quit"): This flags the method as a Spring bean definition, but with the extra feature of invoking WebDriver.quit() when the application context shuts down.
  • @ConditionalOnMissingBean(WebDriver.class): This is a classic Spring Boot technique. It says to skip this method if there is already a defined WebDriver bean. HINT: There should be a test case to verify that Boot backs off properly!
  • webDriver(): This expects three input arguments to be supplied by the application context--a FirefoxDriver factory, a SafariDriver factory, and a ChromeDriver factory. What is this for? It allows us to swap out FirefoxDriver with a mock for various test purposes. Since this doesn't affect the end user, this form of indirection is suitable.
  • The code starts by invoking firefoxDriver using the FirefoxDriver factory. If null, it will try the next one. It will continue doing so until it reaches the bottom, with HtmlUnitDriver as the last choice. If it got a hit, these if clauses will be skipped and the WebDriver instance returned.

This laundry list of browsers to try out makes it easy to add new ones down the road should we wish to do so. But before we investigate, say firefoxDriver(), let's first look at FirefoxDriverFactory, the input parameter to that method:

    class FirefoxDriverFactory implements ObjectFactory<FirefoxDriver>
{ private WebDriverConfigurationProperties properties; FirefoxDriverFactory(WebDriverConfigurationProperties properties)
{ this.properties = properties; } @Override public FirefoxDriver getObject() throws BeansException { if (properties.getFirefox().isEnabled()) { try { return new FirefoxDriver(); } catch (WebDriverException e) { e.printStackTrace(); // swallow the exception } } return null; } }

This preceding driver factory can be described as follows:

  • This class implements Spring's ObjectFactory for the type of FirefoxDriver. It provides the means to create the named type.
  • With constructor injection, we load a copy of WebDriverConfigurationProperties.
  • It implements the single method, getObject(), yielding a new FirefoxDriver.
  • If the firefox property is enabled, it attempts to create a FirefoxDriver. If not, it skips the whole thing and returns null.

This factory uses the old trick of try to create the object to see if it exists. If successful, it returns it. If not, it swallows the exception and returns a null. This same tactic is used to implement a SafariDriver bean and a ChromeDriver bean. Since the code is almost identical, it's not shown here.

Why do we need this factory again? Because later in this chapter when we wish to prove it will create such an item, we don't want the test case to require installing Firefox to work properly. Thus, we'll supply a mocked solution. Since this doesn't impact the end user receiving the autoconfigured WebDriver, it's perfectly fine to use such machinery.

Notice how we used properties.getFirefox().isEnabled() to decide whether or not we would try? That was provided by our com.greglturnquist.webdriver.firefox.enabled property setting. To create a set of properties that Spring Boot will let consumers override as needed, we need to create a WebDriverConfigurationProperties class like this:

    @Data 
    @ConfigurationProperties("com.greglturnquist.webdriver") 
    public class WebDriverConfigurationProperties { 
 
      private Firefox firefox = new Firefox(); 
      private Safari safari = new Safari(); 
      private Chrome chrome = new Chrome(); 
 
      @Data 
      static class Firefox { 
        private boolean enabled = true; 
      } 
 
      @Data 
      static class Safari { 
        private boolean enabled = true; 
      } 
 
      @Data 
      static class Chrome { 
        private boolean enabled = true; 
      } 
    } 

This last property-based class can be described as follows:

  • @Data is the Lombok annotation that saves us from creating getters and setters.
  • @ConfigurationProperties("com.greglturnquist.webdriver") marks this class as a source for property values with com.greglturnquist.webdriver as the prefix.
  • Every field (firefox, safari, and chrome) is turned into a separately named property.
  • Because we want to nest subproperties, we have Firefox, Safari, and Chrome, each with an enabled Boolean property defaulted to True.
  • Each of these subproperty classes again uses Lombok's @Data annotation to simplify their definition.
It's important to point out that the name of the property class, WebDriverConfigurationProperties, and the names of the subclasses such as Firefox are not important. The prefix is set by @ConfigurationProperties, and the individual properties use the field's name to define themselves.

With this class, it's easy to inject this strongly typed POJO into any Spring-managed bean and access the settings.

At this stage, our first test case, fallbackToNonGuiModeWhenAllBrowsersDisabled, should be operational. We can test it out.

Assuming we verified it, we can now code another test, verifying that FirefoxDriver is created under the right circumstances. Let's start by defining our test case. We can start by deliberately disabling the other choices:

    @Test 
    public void testWithMockedFirefox() { 
      load(new Class[]{MockFirefoxConfiguration.class}, 
        "com.greglturnquist.webdriver.safari.enabled:false", 
        "com.greglturnquist.webdriver.chrome.enabled:false"); 
      WebDriver driver = context.getBean(WebDriver.class); 
      assertThat(ClassUtils.isAssignable(TakesScreenshot.class, 
        driver.getClass())).isTrue(); 
      assertThat(ClassUtils.isAssignable(FirefoxDriver.class, 
        driver.getClass())).isTrue(); 
    } 

This preceding test case is easily described as follows:

  • @Test marks testWithMockedFirefox as a JUnit test method
  • load is used to add MockFirefoxConfiguration, a configuration class we'll soon write to help us mock out the creation of a real FirefoxDriver
  • We also disable Chrome and Safari using the property settings
  • Fetching a WebDriver from the application context, we verify that it implements the TakesScreenshot interface and is actually a FirefoxDriver class

As one can imagine, this is tricky. We can't assume the developer has the Firefox browser installed. Hence, we can never create a real FirefoxDriver. To make this possible, we need to introduce a little indirection. When Spring encounters multiple bean definition methods, the last one wins. So, by adding another config class, MockFirefoxConfiguration, we can sneak in and change how our default factory works.

The following class shows how to do this:

    @Configuration 
    protected static class MockFirefoxConfiguration { 
      @Bean 
      FirefoxDriverFactory firefoxDriverFactory() { 
        FirefoxDriverFactory factory = 
            mock(FirefoxDriverFactory.class); 
        given(factory.getObject()) 
            .willReturn(mock(FirefoxDriver.class)); 
        return factory; 
      } 
    } 

The previous class can be described as follows:

  • @Configuration marks this class as a source of bean definitions.
  • @Bean shows that we are creating a FirefoxDriverFactory bean, the same type pulled into the top of our WebDriverAutoConfiguration class via the @Import annotation. This means that this bean definition will overrule the one we saw earlier.
  • We use Mockito to create a mock FirefoxDriverFactory.
  • We instruct this mock factory to create a mock FirefoxDriver when it's factory method is invoked.
  • We return the factory, so it can be used to run the actual test case.

With this code, we are able to verify things work pretty well. There is a slight bit of hand waving. The alternative would be to figure out the means to ensure every browser was installed. Including the executables in our test code for every platform and running them all, may yield a little more confidence. But at what price? It could possibly violate the browser's license. Ensuring that every platform is covered, just for a test case, is a bit extreme. So, all in all, this test case hedges such a risk adequately by avoiding all that extra ceremony.

It's left as an exercise for the reader to explore creating Safari and Chrome factories along with their corresponding test cases.

If we run all the test cases in WebDriverAutoConfigurationTests, what can we hope to find?

Using Spring Boot and Spring Framework test modules along with JUnit and Flapdoodle, we have managed to craft an autoconfiguration policy for Selenium WebDriver with a complete suite of test methods. This makes it possible for us to release our own third-party autoconfiguration module that autoconfigures Selenium WebDriver.

So what have we covered? Unit tests, MongoDB-oriented slice tests, WebFlux-oriented slice tests, full container end-to-end tests, and even autoconfiguration tests.

This is a nice collection of tests that should deliver confidence to any team. And Spring Boot made it quite easy to execute.

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

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