Video Summary and Transcription
A single-page application utilized a server-side BFF layer to simplify authentication and data customization. Testing a BFF-based architecture involves contract testing and tool usage. Challenges arise when mocking server-to-server requests in a client-side and server-side architecture. Separate tests should be written for client-side and server-side components, with contract testing to ensure compatibility. Integration testing for the front-end and server-side can be done by replacing microservices with a sub-server.
1. Building a Single-Page Application with BFF Layer
A couple of years ago, my colleagues and I built a single-page application that obtained data from microservices. While testing was easy, getting data directly from the microservices was complicated. To address this, we added a server-side BFF layer, allowing the client-side application to fetch data from the BFF. This approach simplified authentication and data customization without relying on the client-side.
A couple of years ago, my colleagues and I had to build a new application from scratch. And we decided we wanted to build a single-page application. So our architecture looked some like what you can see here. We had our application, which was a single-page application, which got its data from a couple of microservices.
And so far so good. This had some benefits, like testing was very easy. For testing our whole application in isolation, we could simply mock all the requests to the microservices. But there were also some downsides with this architecture, because we noticed that in some scenarios, getting the data from the microservices directly was rather complicated, because the microservices weren't tailored to the needs of our single-page application. And this is why we decided to adapt our architecture a little bit.
So what we did is, instead of only having a client-side application, we also added a server-side layer, a BFF layer. And now our client-side single-page application didn't get the data directly from microservices, but the client-side piece of our application got its data from the BFF. And the BFF made requests to the services. And this had some benefits. Mostly authentication was easier, for example, because we didn't have to expose an access token to the client-side. And also, we could tailor the data we got from the microservices for the needs of the client-side application, without having to do a lot of data conversion logic on the client-side.
2. Testing BFF-based Architecture
Testing a BFF-based architecture has its pros and cons. In this talk, we will explore the differences in testing between monolithic architectures, single-page applications, and full-stack applications with microservices. We will discuss using an API-first approach with contract testing and tools like Playwright and Spagmatic's stub server feature. Additionally, we will compare test writing techniques, including database seeding, for monolithic applications.
But there was also a downside, because now we couldn't just test our whole application, because when testing our application with Playwright, for example, we can only mock requests made from the client-side in the browser to the server-side. So we could only mock those requests, but we couldn't mock those requests anymore, because those requests are made from the server-side to other server-side applications. And this is not possible with tools like Playwright and Cypress.
So there were some pros and some cons to this approach. And in this talk, I want to show you what are the differences regarding testing when we have, for example, a monolithic architecture, single-page applications like we saw in the first slide, and also single-page applications or full-stack applications in combination with microservices, as we saw in the second slide. We will take a look at how we can use an API-first approach with contract testing to fix the problem we faced when switching to a BFF-based architecture, with not being able anymore to mock the request of the microservices. And we will see how we can do this using Playwright and a tool called Spagmatic and the feature it has, which is a stub server feature.
But first, let's take a look at how we wrote tests or how we write tests when we have a monolithic application so that we can see the differences between the different architectures. So with a monolithic application, typically a LRL, a Ruby on Rails, or for example a Nuxt or Next.js application talking to a database. Monolithic application I would define as having UI layer, business logic, and data layer all in one. So with those kinds of applications, we can use a technique like database seeding to write tests or to have the correct data for our tests.
3. Testing Different Application Architectures
With monolithic applications, tests can be run by seeding the database with the required data. Single page applications can easily mock requests to services. However, when there is a server-side piece within a front-end application that fetches data from microservices, a challenge arises in mocking server-to-server requests.
So for example, here you can see we have a test for a monolithic application. And to run the test, we seed the database where the application gets its data from with a particular data set so everything is set up to run our tests. And a test might look like something like this. This is an example from LRL where we have a seeder helper function and we can create an item seeder class for example.
So far so good. So this is how it works with monolithic applications typically. But how does it work with single page applications? So with single page applications, as we saw before, we might have an SPA and we can write a test for this SPA. And this SPA gets its data from services, from one or multiple microservices, for example. And also, there is really no problem when testing because we can simply mock all the requests that are made to the services.
But now, what about an architecture that, like I showed you before, where we have a client-side part and also a server-side part within a single application, like it's the case with, for example, Next.js or Next.js, or also when you have your own single page application with a custom back-end for front-end, for example, and microservices. So this is important here. Like we saw with monolithic application and also with single page applications without a server-side part, we can use different techniques. But this special case where we have a server-side piece within our front-end application and microservices, and we get our data from microservices, we need or we face a problem.
4. Testing Client-Side and Server-Side Interaction
A test for a single page application written with Playwright can intercept browser-to-microservice requests and directly return a specific response. When dealing with a client-side and server-side architecture, we face a challenge in mocking server-to-server requests. To address this, we can write separate tests for the client-side and server-side components, but we must also test the interaction between the two. Contract testing is a recommended approach to ensure compatibility between the application and microservices.
So a test for a single page application written with Playwright might look like what you can see here. You have this page route helper function provided by Playwright, which we can use to intercept all the requests that are made from the browser to a microservice, for example. So if a request is made to slash API slash items, we tell it that the request should not go through to the real microservice. But instead, we directly return, for example, a single shopping list item.
OK, so those were monoliths and single page applications. But now, what about an architecture that, like I showed you before, where we have a client-side part and also a server-side part within a single application, like it's the case with, for example, Next.js or Next.js, or also when you have your own single page application with a custom back-end for front-end, for example, and microservices. So this is important here. Like we saw with monolithic application and also with single page applications without a server-side part, we can use different techniques. But this special case where we have a server-side piece within our front-end application and microservices, and we get our data from microservices, we need or we face a problem.
An architecture might look something like this, where you have your Next.js application with your client-side components, like your view stuff, and also a server-side piece, which typically are your API routes in a Next.js application. And your client-side code fetches data from the server-side, from the API routes in the Next.js example. And this server-side piece gets its data from microservices. So as I said, here we face a problem that we have two kinds of requests. We have browser-to-server requests, which we can see here from the client-side piece to the server-side piece. But we also have server-to-server requests from our API routes to one or multiple microservices. And when writing tests, we can only mock those requests, but we can't mock those requests here from the server-side to services. What we could do, so one workaround we could choose to use, is that we write tests for the separate pieces.
So we don't have one test for our whole Next.js application or for one feature of our whole Next.js application, but we have separate tests for the client-side part and another test for the server-side part or server-side piece of the application. And this is a valid decision we can make. We can choose to write tests for the different pieces, but there is one problem with this. Because when we go this route, we also have to make sure that we also test the combination of these two so that we test the border crossing from the client-side to server-side. Because if we don't do this, although we might have perfect test coverage because we have tests for the client-side code and separate tests for the server-side code, we can't be sure that those two pieces work together correctly if we don't have a test that sits in between the client-side and the server-side.
And we can have a test like that. For example, contract testing, integration testing, there are different ways how we can achieve this. And contract testing, as it's also in the title of this talk, is a good way to do this. And in fact, for this border here, I highly recommend to use contract testing. So for the border between our application and the microservices, we don't want to have a single test that tests, or maybe just a few, real end-to-end tests. But the vast majority of the features, we don't want to test in a way that we test the whole system, including all the microservices. But we want to have separate tests for our Next.js application and for our services. And here, we can use contract testing to make sure that those two pieces work together also and not only in isolation.
5. Integration Testing for Front-End and Server-Side
For the front-end application, I suggest having a single test for the integration between the client-side and server-side components. Both microservices and the NUXT application are considered single pieces of useful logic. To test the whole NUXT application, including both sides, we can replace the microservices with a sub-server.
But for our front-end application, I think things are different. Although we could use contract testing here as well or some other technique, I think because those two pieces work together to provide a single functionality to a user interface for our users. I think we should have only a single test for those because, in my opinion, there should be testing basically the public interface of a single piece of useful logic. And a single service, a microservice, is a single piece of useful logic because it can be used by other services and applications to get data or trigger some actions.
Also, the NUXT application is a single piece of useful logic, where I think we should have a single test, like we can see here. So in my opinion, we should have a couple of tests which test the whole NUXT application, including the client-side pieces and the server-side pieces, only isolated from the microservices. And the way how we can do this is by replacing the microservices with a sub-server. So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver. So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver.
So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver. So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver. So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver. So instead of mocking the requests made from the client-side pieces, we can do this by replacing the microservices with a subserver.
Comments