Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hi everyone. Welcome. So my talk is called
let's make a pacts. Don't break my API.
So I generally like to start this with a little bit of an icebreaker,
kind of difficult in a virtual situation, but I just like you to reflect
and see if you've ever been in a situation where you
have broken an API for one of your consumers. And I definitely
fall into that category. I've done that on numerous occasions.
And maybe on the flip side of that situation, have you ever had a case
where you've built out a client, you're depending on one or
multiple APIs, and suddenly an API that you depend on stops
working or starts behaving differently and you've received no communication
from the API provider? Again, I've felt that pain more than
once. So what I'm going to talk about today is really how
to address that challenge. So how can we promote features
through our APIs and evolve our APIs safely?
And we'll look at the relationship between the provider and the consumer
and the expectations that exist on both sides in the
modern API landscape.
So a little bit about me my name is Frank Kilcommins.
I'm an API technical evangelist at Smartbear. I'm a software
engineer and software architect by trade, and I'm very
passionate about APIs and the developer experience surrounding
APIs. And to explore the challenges
that kind of I've laid out at the start, what I'll
aim to do is just kind of take a brief look at what's happening across
the API landscape and why things are probably going to get harder
for all of us. We'll look at then how can you design
for future feature promotion through your APIs and
how you can do that safely? We'll maybe critically look
at extensibility. So good extensibility patterns and can
you succeed with extensibility alone? And probably
the TLDR hint here is that you probably
can't. So I'll introduce the concept of bidirectional contract
testing and we'll explore how that can help us safely
evolve APIs. I'll run through a demo showcasing how you
can get started, what it's all about, and then hopefully you'll be able to leave
the conference and leave this talk with some takeaways and a good understanding
around bi directional contract testing.
So microservices are everywhere. Of course, I think for most of
us that have been involved or are involved in APIs or
just general web development, we've seen the exponential growth
of microservices and APIs over the last number
of years. And we can look at all
of the industry reports that come out, the ones that we produce ourselves
at Smartbear, as well as those by our peers across the
industry, like over at Postman. And we see that microservices are
indeed identified as being the main catalyst for the continued growth
across the API landscape. They're really more than
just a fad. So for any of you that might follow kind
of industry analysts like Gartner, you might be familiar
with hype cycles and microservices really have left
the threat of disillusionment. So we're seeing this continued shift
within the most and the majority of organizations from
kind of monolithic architectures to more consumable and capability
focused architectures. And that's where microservices are, of course, very well
suited. They help us unlock the value and the capabilities that's
potentially hidden away within internal systems. So there's more and
more of them being created and managing that
increasing number of APIs and microservices is getting
harder. And it's something that's often referred to as API sprawl.
Now to deal with it. Up until this point in time, most of us
have relied on traditional API management approaches to perform
important activities, from acting as a central catalog
for our APIs to improve discoverability, even enforcing base
levels of assurance on the API. So the minimum amount of
security that must be honored in order for the gateway to be able to process
and pass through that API request, also acting as a hub
for documentation and maybe even most importantly, acting as that
matrix where we can go to, to understand what consumers
are consuming and relying on what APIs and having
kind of also that place where we can go, or that standard process
for authorizing permissions towards APIs and services themselves.
And if you're not experiencing any problem with API management
as you stand today, then the chances are that you probably will in the near
to midterm future. So more microservices
will be created within your organization. And if we look at what
Gartner are saying, they're saying the chances are as much as 50%
of microservices will not be managed by traditional
API management tooling by the year 2025.
So that means there's a high risk that they will become zombie APIs,
ones that are potentially poorly maintained,
susceptible to security vulnerabilities. And more shadow
APIs will pop up because we'll lose track of what APIs are
there. So we'll reinvent the wheel within our organizations.
The cognitive load, unfortunately for keeping track of all of this will fall
upon the teams and it can be very, very easy
to lose track quite quickly. And losing track and not being
aware of who's consuming your APIs can in fact cause
a blindness. And that has several implications. And the
one I'm going to focus on in this talk today is that it's going to
make the safe evolution of your APIs much more difficult.
Now, one of the fundamental ways that we do our
best to not break APIs and to introduce new changes
and new features through APIs is to have strong design practices.
Design first as an approach for microservices and APIs,
ensures kind of thoughtful and let's
say well defined and agreed design before
we go and implement anything. And the earlier you can
bake extensibility as a practice into your API delivery
machine, the more likely it is that you will have long living APIs.
And longevity is a good indicator. It's a good trait
of a successful API. And here's
kind of a little cheat sheet for API extensibility.
And the first one is think about APIs from the
outside in. So think about the consumer experience and what the consumer
needs, and make sure that API that you're delivering is solving that consumer
problem. Use specifications so ones like open API
to describe the surface area and focus on good documentation to allow
the consuming developers to ramp up as quickly as possible,
effectively treating them like a product. Insofar as that you're prioritizing
discoverability, usefulness and usability as
attributes that will really aid and promote
the adoption of your APIs and make it easier for them
to be used and reused, because APIs that existing and are not consumed
are absolutely useless. Moving more onto the tactile
side of kind of things that I would recommend to do. What you need to
do is define your extension points. And what I mean by that is that
you will advertise to your consumers what parts of the API
are stable versus what parts are maybe newer,
maybe mvp, and they're more susceptible to change
in the near term. So don't be afraid to communicate that upfront with your
consumers. It can really go a long way to set the expectations,
then communicate also an extensibility pattern.
So by that I mean you should inform your consumers the
rules that you would like them to follow if they encounter
something new that they haven't seen before and must
ignore, is probably the most common extensibility pattern out there. So that
means that you want your clients to be tolerant, so you're informing them to
say if you encounter something that you do not understand,
then you must ignore it. Your client implementation must not break
just because you do not understand something new.
And then we'll do our work on the provider side to
make sure that anything that we introduce is safe and it's a backwards
compatible change, and it will not materially impact
the functionality of what was there in the previous minor version.
And then when I get onto that concept of minor versions, I think having
a well defined and clearly communicated versioning strategy
is good practice, and it's almost a base expectation on
a consumer side. And semantic versioning is probably the most
well known out there. And that sets again a clear expectation as
to how you deal with patches, how you deal with minor backwards
compatibility changes, and then how you also will deal
with a breaking change. If in fact you have to make a breaking
change to your API, testing for extensibility is great.
Can you then even make it possible for your clients to peek ahead and
test their client implementations to see if they're going to work with the
next version of the API that you're about to roll out, and above all,
communicate. So for each one of these points, communicate every
time you make a change to your API, make sure you have a change
log. Use that change log as a way to connect again with your consumers
to pitch the value of the new features that you're adding and
create that sense of fomo on their side and really pull
them along with you throughout your API journey.
On the don't side, most of these are very self explanatory,
but don't add required inputs to the API because
that would be a breaking change of course. Don't remove outputs
or make them optional. Don't change the type of a property.
So if you have a string today, make sure it's not can int tomorrow,
and then jumping to the last one. Don't be
inconsistent in your process. So every single time you're going through either
an Apache fix, a bug fix, or a new feature enhancement
to your API, follow a consistent process every
single time. So if you do this, if you bake good extensibility
hygiene into your API practices, if you're really
thoughtful around the evolvability attributes, are you
going to be successful? Well, unfortunately,
probably not. So extensibility alone will
not guarantee success. You can only achieve so much with
interface design, no matter how good it is. And even with really
solid extensibility techniques, the chances
are that you will, in some shape or form break
the expectations on a client side. And API
design in general is a skill that's probably lacking
to a certain degree within the landscape or within the API
space. And that's showing up in some of the industry surveys that are out
there as well. So there's a lack of good API designers in
the market, which means it's more difficult for us to achieve high levels of good
extensibility design. That doesn't mean that you
shouldn't follow kind of the cheat sheet or the best practices with regards to extensibility.
You absolutely should, because of course, good extensibility,
good design hygiene allow us to hope for the best for a decent period of
time. But with the acceleration of microservices growth across the
landscape, we also need to be able to prepare for the worst. And we
need to be aware of what's known as Hiram's law.
And Hiram's law is, and I'll paraphrase,
it's with a sufficient number of users of your API,
it doesn't really matter what you promise in that interface design
contract, all observable behavior of your
system will be depended on by somebody. And that absolutely happens.
So you will have some maybe undocumented behavior
within your API that will be understood by consumers and they'll expect
that that will continue to behave like that. And if that potentially was unintended,
and you then address that, whats potentially could be a breaking change for someone,
even though from a provider perspective you've never advertised that the
API was meant to behave in that particular way. And the years
of this can paralyze many providers.
And we have a problem in the industry with major version
proliferation. So as we lose track of who's consuming an API,
what their explicit expectations are, it can
really cause a little bit of a paralyzing effect
on the API provider side. And then there's a knee jerk jerk reaction.
Conf 42 create a major version of your API,
which basically is saying, hey, we're not sure if
this change is breaking or not. So to be safe, we'll create
another version to make sure that any clients that are still connected to the
previous version will remain up and running. And having
multiple, but yet different versions of the same
API incurs real costs on the team. It incurs
cognitive costs with regards to be able to manage these things,
but it also causes actual real costs for teams.
So it's important to kind of break that in mind. So major version proliferation
is a cost problem also for organizations,
and that cost can be felt also on the
testing side of, let's say the
IT teams and the API teams. So zooming in on testing
for a moment I think it's worth noting that if you're also
employing the same testing strategy as you're maybe moving
from a monolithic architecture over towards a microservices architecture,
then probably you will not be successful. So delivering new
value with the appropriate return on investment will
be something that's hard to achieve. And what's
depicted here is a very simplistic picture. So we have a consumer component
and that is connecting and relying with an underlying
microservice, called microservice one. And if I was to
continue to employ, for example, an end to end testing
strategy, which I was quite comfortable with in a monolithic architectural
approach, and apply this to this specific setup
which is depicted here, then every time I would want to make a small change
to the consumer, I would have to stand up all of the underlying
components in order to be able to execute those end to end tests.
And I would also need to be able to manage all of the contextual data
to allow those tests to run effectively.
And that is a very costs exercise and
it leads to what I call an unbalanced
testing pyramid and testing approach.
So if we're over relying on integration testing and into in testing,
the testing pyramid is not really in hits
optimal setup, especially for microservices. So it's expensive
to set up and maintain all of the underlying components and infrastructure.
It's also slow to get feedback. So you might be waiting for
30 minutes, even more for that pipeline to run
in order to get the feedback that you want. It's unreliable. So managing
all of that context and that data can lead to us ignoring
failures. So have any of you ever been in the situation where you
make changes, you run the tests, a test fails.
You weren't really expecting that test to fail because it's not relating to the change
that you made. You run it again, it passes. So then you're saying,
yes, great, we can proceed. So what that actually means is that you've lost
all confidence in the ability for your tests to be
relied upon. So you really have an unreliable and
expensive approach, and you also have an
approach that's not very started. So it's difficult to just test explicitly
what you're changing. You really have to stand up a complex underlying
environment and you can spend a lot of time investigating
false positives.
And this brings us then on to dependency management
for microservices. So how can you manage the dependencies
between all of the provider components, all of the consumer components,
and manage those across the different environments? It can become
quite a headache. So what I have here is a very simplified
CI setup. So two CI pipelines. So this is just for
example purposes, so two cis across
three environments, the CI environment, the staging environment and a
production environment. So when I say CI, think of continuous
integration. So GitHub actions, Azure DevOps pipelines,
Jenkins and so forth. So anytime someone commits a change,
this process will be kicked off.
And here, what we could expect is that once all
checks and balances are passed, we will be able to safely promote
changes through towards production. So you will see that I have different
versions of consumers, different versions of providers between CI
and production. So we're in the process of deploying some features
and some enhancements towards production, and we're
going through the process. Now, what we might
expect is that at each point within these environments,
we'd make sure that the version of the consumer conforms to the
contract or the expectations with respect to the provider on the same
environment. So for example, if we were to run tests on the
CI environment, we would expect to say,
whats consumer v three works well and is compatible
with provider v three? If those tests pass, and then if
other tests pass, maybe they're security tests,
performance tests, whatever the case may be, then maybe we have enough to
satisfy the gate requirements in order to be able to
move and promote towards the staging environment. However, one of
the aspects of microservices and of course communication in general,
is that we don't want to have to promote all components at the same
time. So for instance, if I just wanted to promote consumer
v three in this case towards managing, without promoting
the provider v three towards staging, what else would I need
to check? Well, we'd also need to check, of course, that the
consumer v three works with the version of the provider
which is in staging, which is provider v two. And then equally,
if we wanted to promote consumer v. Two from staging towards
production, we'd also have to make sure that that version of the consumer is compatible
with the version of the provider that's already promoted or deployed
towards production. And what if the inverse
was the case? What if we wanted to promote the provider without
promoting the consumer? Well then of course we'd have to make sure
that the provider v three and CI is compatible with consumer
v two in staging provider v two and staging is compatible with
consumer v one in production and so on and so forth.
So you can see how quickly the complexity can
be increased. And then if you multiply this by more environments, by many
more components, you will get to a situation that can be very difficult
to manage. And one of the things that we really want to do is make
sure that we still can keep the flexibility to allow us to promote
hot fixes, bug fixes and feature extensions without
having to promote all components and all of the dependencies in
lockstep. And this is of course one of the main areas
that contract testing and bi directional contract testing as an approach
can come in to help. And that's what I'm going to jump into now.
But first, let me just plant something in your mind.
And it's a quote from my colleague Beth Scurry, who's one of the
founders of Pact and Pactflow. And she says if you can't deploy services
independently, then you don't have microservices.
And not alone that you have a very costs approach to asserting quality
and testing your implementations. And you have what we call a distributed
monolith. And just in case the red text isn't kind of
advertising it enough for you, that is a bad thing. And it removes
the benefits that are promised by having an ecosystem of
independent, verifiable and deployable microservices.
So bi directional contract testing. So this is an approach
that can really allow us to be able to safely evolve.
And it's one whats also rewards an investment
in a design first approach towards microservices and APIs.
And it can help us avoid many of the pitfalls that we have discussed
up until now. And if you're coming at
this from, let's say, a familiarity with pacts or
even maybe consumer driven contract testing, then this workflow is a
little bit different. So it's schema based rather than a
specification by example. As I mentioned, it really supports a
design first provider workflow, so it's well suited to starting on
the provider side. And it can be described as an ability to upgrade
your existing tools and processes into a powerful contract testing
solution without having to throw away
investments that you've already made in your technical stack.
So you can already leverage open API definitions,
for example, for describing your API contracts on the consumer
side, you can leverage your mocking tools like Cypress, like Wiremark,
et cetera. And then to prove out that your API implementation matches
that open API definition, you can leverage tools
like ready API dread, rest assured postman, and so on and
so forth. I would also say it's a more inclusive way
of contract testing, whereas consumer driven contract testing really
requires and mandates that you have access to the code because it fits more
closely in with unit testing. But bi directional contract
testing does not need direct access to the code, so it
can support a wider demographic of contract testers.
And when I refer to a pact in terms of contract testing, it means the
artifact that is acting as the verifiable contract
between a provider component and a consumer component.
So I do not mean something like an open API definition which
describes the entire surface area of an API.
So what it does is by capturing the interaction expectations between
the software component pairs. So just one integration at a time,
we have an ability to independently verify the
expectations. So this can happen on each side
of a single integration, so we can disregard downstream dependencies
and upstream dependencies, and we can just focus on one integration
at a time so we don't have to stand up a complex environment
for end to end testing. And the
challenge of course that you might have experienced with
certain mocking approaches or test doubles is
you get stale assumptions quite quickly. And here
your assumptions are kept in sync. So with the case of
bi directional contract testing, the assumptions of the consumer
will be validated to ensure that they are a subset of
what the provider supports. And this is really good.
And you might be familiar maybe with Joe Wallace, who's quite a famous tester
and he always advantages for don't mock what you dont own because you
will get drift quite quickly on those assumptions. And the
verification here with bi directional contract testing can
happen asynchronously, so one side does not need the other side
to be available in order to verify.
Once the comparisons on both sides happen, then you can be
confidence that when these components communicate in real life,
that all should be fine. And what I show here on the right hand
side is an example of a packed JSON file. So it stores
who the consumer is, who the provider is,
the interactions that are expected between the consumer and
the provider, and then some additional metadata. So you will also see
some reference here to a packed specification. So there is a defined
specification for describing the format and
the structure of what should be contained within a packed JSON file.
So how does all of this really work? Well, let's run through kind of
an example here. So again, it starts on the
provider side. Now theoretically it can start on both sides, hence the
name bi directional, but it's well suited to starting on the provider side.
So you can bring your API definition, like an open API
definition. Once you can prove
that the implementation of that API is matching what's specified
on the ten, so to say, so you can bring your own provider tool to
do that. Once you have those two things, you can publish
that contract into the packed flow broker.
Then on the consumer side you can continue to unit test as
you will, you can bring your own mocking tool for testing.
Of course you can use pacts DSL, but you're not mandated to. Once you
serialize those unit tests or those expectations into
a pact file, you can then present those to the broker.
And once the broker chaos both sides of that, then the contract comparison
or the cross contract verification is performed by the can I
deploy tool, which is part of pact and packed flows. And we use this feature
really to control and gate a release. So it ensures that what
we're deploying is compatible with any integrations
or any applications that depend on this component in
the environment that we're targeting for the deployment.
And zooming in on this can I deploy check how it makes that kind
of gated decision is it uses whats we call the matrix of
information. So it queries all versions
of the API consumer, all versions of the API
provider, all integrations between all
versions, which ones are compatible with each other and then which environments they've
been deployed to. So in a past life you might have relied on
heavy processes to keep track of this information. Maybe we even had
dedicated teams for this like configuration management or
even change advisory boards. Now this is available instantaneous
for you, so let's run through kind of a demo.
So this would take too long with the actual pipeline.
So I took some screenshots to speed it up a little bit.
But what I've created is a provider API.
It's written in. Net we have a products API which is
just exposing some endpoints to allow us to retrieve some
product information. I'm bringing my own functional testing
tools schema thesis to test whats what I've implemented matches what
I've designed on the consumer side of just a simple console
net core app. And I'm going to again bring my own
unit testing tool and mocking tool, this case wiremark.
And I'm going to set the expectations. We're going to have some pipelines
to publish my artifacts and both consumer
pipeline and provider pipeline will determine if it's safe
to make any of these changes independently.
So here's my API definition.
So it's open API document products API, three endpoints,
get products, get products by id and then also an endpoint to
delete products. So that's my design. Then once I implemented
the code, I can some functional tests through schema
thesis to provider that my implementation is indeed matching
what's described in the design. Once that's caused, I can commit
my code and then I can run my provider pipeline. So here's me
running through just a GitHub action and then I'm leveraging
the can I deploy check through
the GitHub action for can I deploy to verify if
it's safe for me to do this. This passes. Of course this is expected because
it's the first time through the process. I don't have any consumer yet,
so I'm not expecting to break any contract.
So this gets published into Packflow. This is just an example of what
it looks like in packflow. So we have three question marks for the consumer because
we don't have one yet, we have the provider contract. And you can see also
within Pactflow a representation of the open API definition as
well. Jumping to consumer side,
then I have my simple consumer app, and then I create my unit tests
to verify the expectations. So again, nothing special
here. I'm using X unit for unit testing,
standard setup and arrange an act and an assert. I'm taking advantage
here of the fact that Wiremark as a mocking tool has an
actual integration with pact.
So I'm leveraging that component and I'm serializing
the expectations here into a pacts file. What gets serialized
into a pacts file is the name of the consumer. It's all
of the interaction expectations. So in this case I'm expecting to
be able to call the products by id endpoint with a fictitious id.
And if that happens, I'm expecting the provider to return HTTP
404 not found. And at the bottom you can see the name
of the provider that I'm expecting to be able to honor this for me,
again, here's a separate pipeline for the consumer. First time through the process,
everything goes good. What I'm expecting and what I'm
managing from the provider is indeed a valid subset
of the open API definition. And here now we can see
that the pact broker has been updated and everything
is good. The consumer contracts and all of the assertions there
are successful. Now what's going to happen is
I'm going to make what would be regarded as a breaking change.
So the product owner is going to come to me and say, frank, great job
in deploying that API, but I see you have a delete endpoint there,
and that really only should be for the admin API and not for the API
that we're exposing towards external clients. So can you remove that endpoint
now? For me that's a breaking change. I shouldn't be able to do
that. I should version an API. But what
pactflow and bi directional contract testing allows me to do
is it allows me to make that change safely because I can fully
understand and I have full visibility into how the consumer
is expecting my API to provide for them
so what their expectations are. And I know that they are not
using the delete endpoint. So therefore it's safe for me
to make what would traditionally be a breaking change.
And you can see that the pact within the
pacts broker between the provider and the consumer is still there,
it's still valid. But you can see that the provider contract itself
has updated and now it only has the two get endpoints.
Now it's all well and good, seen all green. But what would happen if I
was to try to make a change that would actually break my consumer
client implementation? So here's another example of
this. So the product owner has now asked me to make some changes because
client errors 404 are hindering his reports
and making him look bad. So he's asked me hey,
can I change those 404s maybe into something different?
I'm not going to debate it too much with him. What I'm going to do
is check is this actually going to be a breaking change?
And what I can do is make that change and instantaneously
see that it would break the expectations on the consumer
side because they are expecting a 404 to be returned. And if
I try to change that, I will get notified that I cannot
deploy this and it will prevent me from making this breaking change for
the consumer. I'll see that in the CI process and
then I'll also see it in Pactflow itself.
And I can see that the contract comparison has failed and
I see explicitly why it's failed. So the provider
code no longer returns a 404 and
that is expected by the consumer. So I get explicit information
into that and the matrix of information that's continuously
queried by the can I deploy check is also updated.
And here we can see the top one has failed
and we can see that the environment is in a so I was not allowed
to deploy whats change into production.
So hopefully you'll see that by introducing a technique like
bi directional contract testing towards your microservices approach,
it can reduce your need for end to end testing
and integration testing and it can significantly rebalance
your costs with regards to testing and enables you to
really safely evolve microservices. You'll have an approach that's
simpler, it's cheaper, it's faster, it scales
linearly as your number of microservices scale, and it allows you
to make sure that you can keep that flexibility and deploy independently.
And with regards to how this benefits a
design first approach one of the main challenges that we have
with API design first is that you lose visibility
quite quickly once you get an API into production
and you start ramping up your number of consumers and it can be really
hard to make informed decisions around what changes
will be breaking, what changes will not be breaking. All you can do
is assume that the full surface area of your API definition is deemed
on by somebody but with contract testing it
gives you that full explicit insight into how consumers are
consumer your API and it lets you have a more pragmatic rather
than dogmatic approach to your API versioning
strategy. So hopefully you see the benefits. Hopefully you enjoyed my
talk. Thanks very much for attending. If you want to connect and reach out
please do. Here are my details and here's how you can get up
and start playing with contract testing and also some useful
resources. Whats helped me frame some of this talk.
Thank you very much.