This information may be of limited use to most people. I've been working on a test framework. Its purpose is to, in as little code as possible, provide the standard patterns for creating configured test resources for multi-threaded tests.

As such I need to be able to inject beans into my test instances using the prototype scope. That's relatively easy to do. However, it struck me the other day that an API test harness may require the ability to talk to n different APIs supporting an application. I'll explain:

  • We have the front end - on one address - for Selenium to hit
  • We have the API gateway with our serverless applications behind it
  • We have some containerized servers, for when that pattern is appropriate, behind load balancers

To black box test this application we have three different endpoints. While we could just test it through the UI, it makes sense to have some slightly lower-level tests to probe bits of the API directly in much the way the browser does.

Bear in mind, these are just smoke tests, to check post deployment. I'm not going crazy!

How Did I Want To Configure It?

I wanted to use a simple baseUrl property as the default address for Selenium and RestAssured to point at. In many cases/tests, this would be enough. Just point it at The URL. In the background, there's a pattern to assign a Selenium Driver from a pool when a test tries to inject a page object, and it was easy to use the prototype pattern to create a new instance of the RequestSpecification class, with the base uri already set.

Note: we NEED the prototype pattern here, as another test may start modifying the request specification, on another thread... this means bad separation of concerns otherwise. Bad times!

So the challenge is how I can specify the other baseUrls and have them injectable as other RequestSpecification prototypes.

With the following config I could do the obvious stuff:

 @Bean @Scope("prototype") public WebDriver webDriver() throws Exception {     return DriverPool.INSTANCE.allocate(); }  @Primary @Bean @Scope("prototype") public RequestSpecification mainRestClient() {     return given().baseUri(ConfigurationLoader.getConfiguration().getBaseUrl()); } 

Note, I've given a clue about the multiple RequestSpecification objects, by tagging this one @Primary. Now, when a test class needs either of the above:

 @Inject // synonym for @Autowired private WebDriver webDriver;  @Inject private RequestSpecification requestSpecification; 

In both cases, the right object is wired in, configured correctly and only used by this test on this thread.

Why Configure It?

The tests will run against multiple environments and can have their environment specifics driven by environment variables, used as part of a bootstrap process using Lightweight Config.

So How Can We Have Dynamic Prototype Beans?

Let's work backwards from the injection point:

 @Inject private RequestSpecification mainRequest;  @Inject @Qualifier("backEnd") private RequestSpecification backEnd;  @Inject @Qualifier("frontEnd") private RequestSpecification frontEnd; 

Ok. That seems nice enough. Now how do we configure the framework?

 baseUrl: http://somebase.example.com restBaseUrls:   backEnd: https://www.backend.com   frontEnd: https://www.frontend.com  

That seems legit... so now how do we get the beans to exist?

Note: the config.yml above is some custom configuration stuff in my test rig. What follows is the spring magic that backs it up.

If I wasn't making a test framework, I could do this stuff explicitly in a Spring Configuration as I did with the other beans. As I want it data driven then I have to solve the fact that the code I write in the framework is separate from the test pack using and configuring that code.

I could have avoided this by asking the adopter to write more boilerplate, but boilerplate sucks.

So:

 // the main configuration class is a bean factory post processor @SpringBootConfiguration public class TestConfiguration implements BeanFactoryPostProcessor {     @Override     public void postProcessBeanFactory(       ConfigurableListableBeanFactory beanFactory) throws BeansException {         Map<String, String> urls =           ConfigurationLoader.getConfiguration().getRestBaseUrls();         if (urls == null || urls.isEmpty()) {             return;         }          urls.forEach((name, url) -> registerPrototypeFor(name, url, beanFactory));     }      private static void registerPrototypeFor(String name, String url,           ConfigurableListableBeanFactory beanFactory) {         BeanDefinitionRegistry registry = ((BeanDefinitionRegistry)beanFactory);         String factoryBeanName = name + "Factory";         beanFactory.registerSingleton(factoryBeanName, new RestCreator(url));          GenericBeanDefinition beanDefinition = new GenericBeanDefinition();         beanDefinition.setBeanClass(RequestSpecification.class);         beanDefinition.setLazyInit(false);         beanDefinition.setAbstract(false);         beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);         beanDefinition.setFactoryBeanName(factoryBeanName);         beanDefinition.setFactoryMethodName("create");         registry.registerBeanDefinition(name, beanDefinition);     } }  // this is a factory bean, capable of building rest assured // objects for given urls public class RestCreator {     private String url;      public RestCreator(String url) {         this.url = url;     }      /**      * Create a {@link RequestSpecification} driven by       * {@link Configuration#getRestBaseUrls()}      * @return a new request specification      */     public RequestSpecification create() {         return given().baseUri(url);     } } 

There's a lot to unpack in the above code. The essence is this. We register a singleton factory bean for each different variation. Then we register a prototype bean that uses the factory's create method. We name each bean after the identifier in the config file, so the writer of that file knows the qualifier to use for the prototype bean, since they chose it.

On Frameworks

Frameworks are big hefty bad things you don't want to maintain.

Standards are good. A simple configuration, built from relatively well-known pieces, that's reused everywhere... AND FITS... is a very useful thing.

I'm trying to make it easy to share a standard across some projects.

So far, so good.


This free site is ad-supported. Learn more