Part 1. First Steps
Welcome, friend.
Those words adorn the welcome mat near my front door, and they are the first words you will see when you visit the mountebank website (https://www.mbtest.org). I’d very much like them to be the first words you read in this book, as I welcome you to the wonderful world of service virtualization in general and mountebank in particular.
This first part aims to provide context behind that introduction, setting the stage for introducing mountebank as part of your testing and continuous delivery stack. Because one of the main drivers behind service virtualization is the increasingly distributed nature of computing, chapter 1 starts with a brief review of microservices, with a focus on how they change the way we test software. It puts service virtualization in context and provides a gentle introduction to the main components of mountebank.
Chapter 2 demonstrates mountebank in action. You’ll get some dirt under your nails as you write your first test using service virtualization, providing a simple launching point to explore the full capabilities of mountebank in part 2.
Chapter 1. Testing microservices
This chapter covers
- A brief background on microservices
- The challenges of testing microservices
- How service virtualization makes testing easier
- An introduction to mountebank
Sometimes, it pays to be fake.
I started developing software in the days when the web was starting to compete with desktop applications in corporate organizations. Browser-based applications brought tremendous deployment advantages, but we tested them in almost the same way. We wrote a monolithic application, connected it to a database, and tested exactly like our users would: by using the application. We tested a real application.
Test-driven development taught us that good object-oriented design would allow us to test at much more granular levels. We could test classes and methods in isolation and get feedback in rapid iterations. Dependency injection—passing in the dependencies of a class rather than instantiating them on demand—made our code both more flexible and more testable. As long as we passed in test dependencies that had the same interface as the real ones, we could completely isolate the bits of code we wanted to test. We gained more confidence in the code we wrote by being able to inject fake dependencies into it.
Before long, clever developers produced open-source libraries that made creating these fake dependencies easier, freeing us to argue about more important things, like what to call them. We formed cliques based on our testing styles: the mockists reveled in the purity of using mocks; the classicists proudly stood by their stubborn reliance on stubs.[1] But neither side argued about the fundamental value of testing against fake dependencies.
1
You probably have better things to spend your time on than reading about the differences between classicists and mockists, but if you can’t help yourself, you can read more at http://martinfowler.com/articles/mocksArentStubs.html.
It turns out when it comes to design, what’s true in the small is also true in the large. After we made a few halting attempts at distributed programming, the ever-versatile web gave us a convenient application protocol—HTTP—for clients and servers to talk to each other. From proprietary RPC to SOAP to REST and back again to proprietary RPC, our architectures outgrew the single codebase, and we once again needed to find ways to test entire services without getting tangled in their web of runtime dependencies. The fact that most applications were built to retrieve the URLs for dependent services from some sort of configuration that varied per environment meant dependency injection was built in. All we needed to do was configure our application with URLs of fake services and find easier ways to create those fake services.
Mountebank creates fake services, and it’s tailor-made for testing microservices.
1.1. A microservices refresher
Most applications are written as monoliths, a coarse-grained chunk of code that you release together with a shared database. Think of an e-commerce site like Amazon.com. A common use case is to allow a customer to see a history of their orders, including the products they have purchased. This is conceptually easy to do as long as you keep everything in the same database.
In fact, Amazon did this in their early years, and for good reason. The company had a monolithic codebase they called “Obidos” that looked quite similar to figure 1.1. Configuring the database that way makes it easy to join different domain entities, such as customers and orders to show a customer’s order history or orders and products to show product details on an order. Having everything in one database also means you can rely on transactions to maintain consistency, which makes it easy to update a product’s inventory when you ship an order, for example.
Figure 1.1. A monolithic application handles view, business, and persistence logic for multiple domains.
This setup also makes testing—the focus of this book—easier. Most of the tests can be in process, and, assuming you are using dependency injection, you can test pieces in isolation using mocking libraries. Black-box testing the application only requires you to coordinate the application deployment with the database schema version. Test data management comes down to loading the database with a set of sample test data. You can easily solve all of these problems.
1.1.1. The path toward microservices
It’s useful to follow the history of Amazon.com to understand what compels organizations to move away from monolithic applications. As the site became more popular, it also became bigger, and Amazon had to hire more engineers to develop it. The problems started when the development organization was large enough that multiple teams had to develop different parts of Obidos (figure 1.2).
Figure 1.2. Scaling a monolith means multiple teams have to work in the same codebase.
The breaking point came in 2001, as the company struggled to evolve pieces of the application because of the coupling between teams. By CEO mandate, the engineering organization split Obidos into a series of services and organized its teams around them.[2] After the transformation, each team was able to change the code relevant to the domain of their service with much higher confidence that they weren’t breaking other teams’ code—no other team shared their codebase. Amazon now has tremendous ability to develop different parts of the website experience independently, but the transformation has required a change of paradigm. Whereas Obidos used to be solely responsible for rendering the site, nowadays a single web page at Amazon.com can generate over a hundred service calls (figure 1.3).
2
See https://queue.acm.org/detail.cfm?id=1142065 for details.
Figure 1.3. Services use different databases for different domains.
The upshot is that each service can focus on doing one thing well and is much easier to understand in isolation. The downside is that such an architecture pushes the complexity that used to exist inside the application into the operational and runtime environment. Showing both customer details and order details on a single screen changes from being a simple database join to orchestrating multiple service calls and combining the data in the application code. Although each service is simple in isolation, the system as a whole is harder to understand.
Netflix was one of the first companies of its size to migrate its c...