Sunday, August 6, 2017

Making Cloud Code Testable

Developers seem to assume that just because their application code may now interact with the cloud (e.g. read/write data from AWS S3 buckets, invoke AWS lambda functions, email via AWSD SES, send or receive AWS SMS messages, etc.) that it's no longer reasonable or easy to structure their code in a testable way without access to the cloud. Yes, my examples are AWS centric. The principles in this blog post apply to Azure or Google Cloud code as well.

By "testable" I mean unit tests that don't require access to the cloud to run. The purpose of such tests is to test logic in your code; not to test that code has the ability to connect to outside resources. Testing the ability of your code to connect to outside resources is the purpose of integration tests

Inject resources into application code that can't exist without the cloud, network, or external resources of any type. When your application code creates/instantiates these resources, that code can no longer be run without those resources. A previous version of a class in listing 1 violates this advice and isn't testable.

Listing 1: AntiPattern Example -- Don't do this at home!
Listing 1 isn't unit testable as it requires access to AWS to exist. By the way listing 1 is coded, any test on it's logic really is an integration test. A much better way to write this class can be found in listing 2.

Listing 2: A more testable way to write listing 1

Listing 2 is much more testable that listing 1 as it no longer depends on the environment in which the AmazonS3 instance was created and can easily be mocked. That instance can easily be mocked as I did in the unit test for this example class. I've a snippet of the Mockito code used to test this class in listing 3.  In fact, test coverage is 100% for line, branch, and mutation coverage for this class.

Listing 3: Mocking an AWS resource for unit tests.

Incidentally, there are numerous ways to inject cloud-dependent resources. You don't need to inject it on construction as I did in my example. For instance, the Spring Framework or Guice can also do that injection for you.

Another way to look at listing 2 is that it uses the architectural principle of "separation of concerns". Listing 2 separates the environmental concern of creating cloud dependent resources from the logic of listing the content of an AWS bucket. This makes the resulting classes much more focused and less complex. This concept begs a question: Where do you create the AmazonS3 client and can you do that in a unit testable way? The short answer is that you really can't. However, you can localize and minimize the amount of code that isn't unit testable.

Localize the creation of resources that are dependent on the cloud or any other external resource.  Generally, my projects end up with a "factory" class that handles instantiations that aren't unit testable.  To continue the S3 bucket list example, I'd envision a class like the factory presented in Listing 4.

Listing 4: AWS S3 Client Instantiation Example
This tactic minimizes the amount of code that isn't unit testable. Furthermore, this code doesn't generally contain complex logic leaving the more complex logic to classes that can be unit tested. 

Incidentally, I did also construct a unit test written using JMockIt as a development team I'm working with had adopted it. As it happens, once JMockit decorates a class with stubbing code, it's not possible to use it directly anymore (bug documented here).  I commonly want to stub an InputStream or OutputStream only for the purpose of testing my code handling exceptions. It seems that with this bug, this isn't currently possible using JMockIt. the bug is very irritating and applies to a significant percentage of the unit tests I write. I'm personally sticking to Mockito for now.

No comments:

Post a Comment