Rocky Mountain Ruby 2014

Micro Testing Pains

Micro Testing Pains

by Marcos Castilho

In the talk titled "Micro Testing Pains," speaker Marcos Castilho addresses the complexities of testing within microservices architectures. He begins by discussing the challenges of the traditional monolithic approach, characterized by cumbersome codebases that are difficult to manage and test. Castilho highlights the advantages of microservices, including:

  • Single Responsibility: Each microservice is designed to handle a specific functionality, enabling better organization and team collaboration.
  • Flexibility of Technology: Different services can be built using various programming languages, allowing for innovation and adaptability.
  • Separation of Workflows: Teams can work on distinct services leading to improved focus and reduced contention among team members.

However, he acknowledges that transitioning to microservices introduces challenges, particularly in behavioral testing and integration. He emphasizes the importance of effective testing practices to properly validate integration between services. Castilho explores several approaches to testing in microservice environments:
- Local Instances: Running all required services locally, though this can be complex to manage.
- Shared Environments: Utilizing a development or QA environment for testing, which can lead to inconsistent results due to flakiness in tests.
- Mocking/Stubbing: Using stubs to simulate service behavior, but warns of the pitfalls of relying too much on unrealistic stubs.
- VCR Technique: Capturing and replaying HTTP interactions, while noting that this can become complicated with numerous services.

Castilho introduces the concept of Consumer Driven Contracts (CDC) as an effective strategy for enhancing testing in microservices. Under this framework:
- The consumer defines their needs from the provider's services, creating a specific contract that clarifies expectations.
- Both parties collaborate on the contract, ensuring clearer communication and alignment as services evolve.
- This practice encourages teams to focus on the consumers' requirements, fostering better team collaboration.

In conclusion, while microservices offer flexibility and enhanced organization, they come with significant testing challenges. Consumer Driven Contracts present a viable solution that can streamline integrations and improve collaboration between teams.

00:00:27.760 Hello everyone! My name is Marcos Castilho, and you can find me online as marcus at pretty much any platform. I work for a great company called Totrex, who is kindly sponsoring my trip here. I'm coming all the way from Scotland, although I'm originally from Brazil. And just to clear the air, I actually like microservices! But throughout this talk, it might seem like I don't. So let's talk about the mythical monolith.
00:00:47.840 Consider the monolith: it's like a billion lines of code that does everything your organization needs. It seems like a common scenario in Ruby: we’ve moved past the initial excitement phase, and now we're just focused on getting things done. Startups begin small, building services that grow as they add more functionalities. Eventually, you reach a point where you have a massive codebase that does everything—something we can probably agree isn’t ideal. I'm sure everyone here has horror stories about dealing with such codebases. You touch one line, and suddenly you break ten classes nearby, all without knowing what you've done. The term 'monolith' genuinely fits, similar to finding something massive and bewildering in the woods—who built this? So, clearly, this isn't the best approach.
00:02:00.880 To escape from this dilemma, many companies are turning to microservices. So what exactly is a microservice? It's a small codebase designed to handle a single, well-defined functionality. The idea is to apply the Unix philosophy of small utilities working together. The term is relatively recent; it gained traction around 2011, and while it's often discussed in the context of Java, I'm starting to hear more about it in Ruby conferences.
00:02:43.360 In a microservices architecture, services usually communicate through a RESTful interface, leveraging HTTP. Unfortunately, in our industry, that often means adhering to the limitations of HTTP. The goal is to implement all the good practices of single responsibility principles within a service-oriented architecture. Microservices aren't entirely novel; it's about services being constructed appropriately.
00:03:06.560 Transitioning from a monolith to multiple microservices presents various benefits. These services are easily replaceable due to their small size, allowing you to discard a service you don't like and create a new one without worrying about the entire ecosystem. This enables technology flexibility; a Ruby service can communicate with a Java service, which, in turn, talks to a Go service, etc. Additionally, their simplicity ensures the entire codebase fits in your head. They offer a natural separation of workflows, allowing companies to allocate teams to specific services without stepping on each other's toes too often. They communicate via defined interfaces, streamlining the organization.
00:03:40.640 However, as with all trends, adopting microservices doesn't come without its complexities. Deploying a single app is hard enough; imagine deploying 30 to 50. Performance might take a hit, and navigating the intricacies of networking among numerous microservices can become sluggish. Security also becomes an issue, as you can't just shove data into a session anymore; you need robust methods of authentication and proper monitoring. These challenges can quickly complicate the landscape once you enter the microservices realm.
00:04:20.160 When it comes to testing, especially acceptance or integration tests, many issues arise. These tests check how well one service integrates with another. I believe that tests are only as good as their failure messages. When testing microservices, especially through a UI, you might encounter vague messages like 'expected to find element item but couldn't.' This isn’t helpful, and after running tests in your CI pipeline for a while, you could still end up frustrated with unclear failures. Such shortcomings lead to a significant challenge.
00:05:06.240 Now, let’s address a critical question: how do you ensure you’re not disrupting someone else's day? When defining APIs for services, they need to be well-structured, but in practice, they can often change rapidly. If the customer of those services is your own company, we might not maintain the same professional standards as we would with a public API. This can complicate testing, especially when different teams are working with different technologies.
00:05:44.560 Over time, I’ve encountered this problem frequently while working with microservices, particularly in creating acceptance tests. One approach I tried was running all services in your ecosystem on your local machine before testing. While this can work, managing various technologies and ensuring everything runs smoothly on a local machine can become complex, especially as your CI pipeline grows.
00:06:10.000 Another approach is to run tests against a shared environment, like a development or QA environment where services are continuously running. However, many companies struggle to maintain a consistent production environment, leading to flakiness in the tests. Flaky tests can erode the confidence of a team. You might find that a test passes one moment and fails the next without any clear reason why.
00:06:44.000 A third approach might involve creating mocks or stubs for each service, allowing you to test against those stubs. This could be done using automated tools or custom code. While this can work, it often leads to a situation where you're trapped in a 'fantasy stub land,' where everything appears to function flawlessly, but in reality, you need to keep these stubs in sync with any service changes.
00:07:44.360 Another method is to utilize VCR, a technique that records HTTP interactions and allows you to replay them for tests. This can work well when talking to stable APIs like GitHub or Facebook. However, with numerous services in play, the recorded interactions can quickly become noisy and challenging to interpret. Each commit might create several changes that could confuse those working on the project.
00:08:25.280 Here's a sad story: while working on a microservices project, my team had to make changes to a rapidly evolving API without proper versioning. Every time we wanted to change something, it involved searching for dependencies and asking various teams if they were using specific functionalities. This ad hoc approach seemed really inefficient, and ultimately it felt ridiculous. I went on a quest to find better solutions.
00:09:07.680 I stumbled upon the idea of using executable contracts. A contract in this context refers to the specific functionality one service needs from another. It differs from the service API, focusing only on what's necessary for that interaction. For instance, if service A needs data from service B, the contract could specify a single field in an HTTP call that service A requires. It’s all about defining what’s necessary without overwhelming the service.
00:09:37.040 The process begins with service A, the consumer, defining the required functionality from service B, the provider. This involves drafting a contract that outlines the needed specifications. Both sides can engage in discussions to refine this contract before implementation. The beauty of consumer-driven contracts lies in their mutual benefits: both parties can evolve their services while staying aligned with the established agreements, maintaining important communication throughout.
00:10:36.720 By using contracts, you create a clearer understanding of dependencies, which is crucial when changes occur. For the provider team, knowing who uses their services can facilitate easier updates or even eliminate unused endpoints. The same benefits apply from the consumer’s perspective: clear contracts lead to well-defined expectations for the functionality they depend on. If something changes unexpectedly, the explicit nature of the contract provides a specific error message.
00:11:25.520 Consumer-driven contracts encourage teams to prioritize the actual needs of the users of the services. Instead of focusing solely on what cool capability a service can provide, the emphasis shifts to understanding what consumers truly need. This not only enhances the quality of communication between teams but invites increased collaboration and discussion.
00:12:00.640 In conclusion, microservices offer many advantages but come with their own set of challenges, particularly around testing. Executable contracts open up the possibility for effective testing in isolation, while consumer-driven contracts facilitate harmonious service evolution and communication among teams within the organization. If you wish to learn more about this approach, I'll be sharing additional resources and links in the presentation later.
00:12:36.480 Thank you for your attention!
00:12:39.000 You are welcome!