Testing in serverless – challenges and approaches

To Nha Notes | Oct. 21, 2023, 5:17 p.m.

Local manual tests

While strictly not part of the testing pyramid, it is common for developers to test their code manually by running it locally and testing whether something breaks or works. In the case of Lambda, there are a few ways to do this:

  • AWS Serverless Application Model (SAM) CLI: AWS SAM is a framework that AWS provides to develop serverless applications effortlessly. This is similar to the serverless framework, but only for AWS. When you are developing with SAM, it provides you with a CLI and you can invoke the Lambda function locally using the sam local invoke command.
  • Similarly, the serverless framework supports local invocation using the serverless invoke local command.
  • Zappa, while having an invokeoption, can only be used to invoke the deployed function remotely. It is a similar case with aws lambda invoke.

Other serverless frameworks can do similar invocations for other vendors as well as for different languages. Depending on your development framework, you can try those out. Now, let us look into serverless.

Unit testing for serverless

Unit tests should be run on the developer machine or the CI/CD pipeline. They shouldn’t have any interaction with external services. With FaaS code such as Lambda and other platforms, your entry point to the FaaS is always a handler function. All other functions written in the code are indirectly invoked by the handler or other functions. To better tune your code for testability, it is advised to separate the business logic into different functions and the interactions with external systems shouldn’t be part of these functions.

It is easier to test the business logic functions if they are not dependent on external systems. You can pass test values or use mock objects to achieve this. Mock libraries and platforms provide you with libraries that can simulate the behavior of cloud APIs. For AWS, there are a few popular options. They are listed as follows:

  • Moto: Moto is a Python library that can be used to mock the AWS Python SDK called boto. If your Lambda function is written in Python, you can include this in your Python test suite and mock the AWS API calls. If you are using another language, Moto allows you to spin up a local endpoint against which you can test your API calls. AWS SDKs provide an option to pass a custom API endpoint for all their functions. You can use this to redirect your API calls to the local Moto endpoint. For more information, refer to https://github.com/spulec/moto.
  • LocalStack: LocalStack is a Python framework that allows you to spin up a local AWS API endpoint. Unlike Moto, it doesn’t provide a mock library that you can include in the test suite. So, you have to pass the localstack endpoint (which you can run on your developer box or CI/D infrastructure) to the AWS SDK functions. LocalStack also provides a paid cloud service for more detailed testing.
  • Generic mocking of functions: Test libraries in all languages have mock libraries that can be used to change the behavior of a function. For Python, there is a Python unit test library that also has a mock module within it. All mock libraries provide their own decorators that can be used to modify the behavior of a function you want to test.
  • Serverless offline: The serverless framework provides a serverless offline plugin that emulates Lambda and API Gateway.
  • AWS SAM Local API: The SAM CLI allows you to spin up an API endpoint locally.
  • AWS provides local installations of some of its services, such as DynamoDB, which you can spin up to test locally. For S3, there is open source software such as MinIO and Riak CS , which have S3-compatible APIs.

There are more ways to do unit tests, but these should give you a good idea. Also, other cloud vendors have similar options, but not as many as AWS has.

Integration tests

Integration tests allow you to test your interactions with other components. With FaaS, these components are often other serverless services in the cloud. While we can mock the service call unit tests, integration tests should invoke their tests against the integration points (other serverless services or external systems). To do this right, the developer should set up a test environment within the cloud. This should cover all the cloud services that the FaaS code is interacting with. The typical workflow is the following:

  1. Create the required cloud services in a cloud account – preferably in an account dedicated to testing/development.
  2. To create these services, make use of an IaC framework. Define your requirement and configurations in the IaC code and launch the test stack in the cloud account.
  3. Most of the serverless frameworks have the built-in capability to provision these services, either by using cloud APIs or the IaC code internally. If you are developing your FaaS with one of those frameworks, it will be much easier to set up the test environment.
  4. Ensure that your code takes the endpoint details of all cloud services (such as S3 bucket names and DynamoDB table names) from environment variables. This way, it will be easy for you to configure your functions to run against any environment.
  5. When writing test cases, avoid using mocks – which should be reserved for unit tests.
  6. Business logic testing should also only be done in unit tests. Integration tests are supposed to flush out issues with integration points.
  7. If your function talks to serverless data backends such as S3 or DynamoDB, ensure to load the required test data and clean it up after the test is done. Ideally, this should be part of your test fixture.
  8. All function executions are triggered by events. When triggering the functions as part of the test, ensure you have stub events that you can pass to your functions. Depending on how the function has been triggered, the structure of the event will vary. Check out the corresponding cloud documentation to understand the event structure when writing your integration tests.

These are some of the broad guidelines for running integration tests in serverless applications. You should also follow up with acceptance/end-to-end testing once the function is deployed to your testing environment. While you might initially run unit and integration tests manually, this should ultimately be migrated to a CI/CD pipeline. We will take a quick look into CI/CD pipelines for serverless in the next section