To this Tech Week's Tech Excellence Session, my name is Olli and as you probably already know by now, our vision is to raise the bar of technical excellence across the world. Who are we? Well, we are Valentina, who is our founder and organizer of Tech Excellence, and Daniel and Alina and me, we are both co-organizers and also hosts. We already had a great couple of speakers in the past and we will also continue to have lots of great speakers in the future. You can follow us on all the shown media, social media like Meetup, YouTube, LinkedIn.
It's not Twitter anymore. It's X, GitHub, and also Discord. Our sponsor is Optivem and our partner is QEunit. And now I am really happy to be joined by Nick Schumacher.
Nick is a software engineer and an architect with over 17 years of professional experience. He currently works at Payment System at George, the digital banking solution of Erste Group, where he actually practices pair programming and mob programming and uses these practices on a daily basis, which is... very rare and which we are really happy that this actually exists.
Beyond programming, Nick also trains web developers in-house at enterprise companies all over the world and also on LinkedIn. And today I'm really happy that he will show us how we can actually ship React app with zero bug policy using test-driven development. And this is also something that I personally always struggle with. But before I hand over to Mick, I want to remind the audience that you are very much encouraged to write down any comment, observation or question you have in the comment section down below. And after Nick's talk, we will have the time to discuss them all together.
But now I actually hand over the mic to Nick. So Nick, the stage is yours. Hello, everybody. Today I want to make a deep dive into building React app using test-driven development and introduce you to zero bugs policy and how we build a system to maintain the quality of the software at very high level in the digital banking solution that I work on. All right so First, there are many product development problems, but let me list first significant economic ones that I think our job as software developers can impact.
Can impact positively or can impact negatively. For example, shipping new features versus fixing bugs is a common problem in our industry. When we don't have a process that enables us to ship with quality, then we will be Always in the season, you know, whenever we deliver a new feature, some already existing one breaks and we have to stop moving on until we fix bugs.
Or another problem is when we don't have a system to design a simple code, code that is easy to change, for example. At some point, we will go and ship the next feature much longer. It will take much more time.
We put in another hack in favor of moving quicker now, but it's a dead end. Hack after hack and some software design turns unmaintainable. Another problem is when we want to work on a legacy code full of hacks, people get demotivated, especially when it's under pressure.
People can lose motivation when the system is hard to extend, hard to enhance with new features, new business requirements. Therefore, skilled talent retention is low, becomes low. Onboarding new hires takes time and it's actually very expensive for the company to have people leading the project after six years and onboarding new people again. Shipping software is also a responsibility. For example, in my context, I work on digital banking app and how would you feel if one day when opening your banking app you see a screwed up account balance or It's negative, by mistake, because a development team ship the code with a bag.
How would you feel? Probably you will lose trust in this bank, won't you? Losing trust is losing a customer.
Therefore, it's a high responsibility of what we ship, what we work on every day, and to deliver it to the people. I think we could tackle this kind of problems by having a system that enables to ship software that works. A system that I call test-driven development.
All right, here is a small real life project we will be building today. Let's learn business requirements a bit. So the idea is to enable the user to explore the international bank account number details. when the bank account number is a valid one, or provide feedback to the customer when the IBAN is invalid.
Because our team is doing behavior-driven development, you can see that requirements here are nailed down in a very comfortable way. So first acceptance criteria would be to create a page with a form with IBAN input and a submit button. so we could validate the IBAN. Then the IBAN that customer entered into the input field we are supposed to send to the API and then based on what API returns there is some criteria how to display different validation results in the list here.
Otherwise if API throws an error we're supposed to display some kind of error message as is seen here. At the right of the screen, I have API specification also available within this task. And the idea will be to call get method for validate endpoint within our API by providing the IBAN that the customer has been added into an input field.
And the response, the shape of the response will look like this, the API. If the IBAN is valid will provide back the IBAN itself and some flags that will help us to actually draw these results in the middle of the page. Plus it also will provide you a trust score for a particular bank. Of course we have some test data to validate whether everything works correctly or not. So first before we start actually The process of test-driven development, I want to introduce you to the initial project structure.
And here what we have is a React project built with Vita. I'm using TypeScript here. Beyond that, I'm having an app which outputs at the moment just hello world, nothing much, we will build. On top of that, I have some internal design systems, a system with already prepared set of React components that I can use. They are using Tilewind.
Here you see probably familiar syntax of class names, the Tilewind outputs. It will help us to compose actually our application. If we look at TAC and JSON, what is important, here are some scripts to start my app, but what I want to put your attention on that we have a command to run unit tests. We will be using VTest for that. There is a command to run integration tests.
For that, we will be using Cypress components testing. That's a tool that enables to do UI testing. And we have a command to run test end-to-end and for that we will be using another approach with Cypress not components testing but end-to-end testing they have different approaches I will show you during the development so already three type of tests that we will employ today will be fun okay back to my slides now when we look at these business criteria or business requirements described in this project, in this task. Could we try to think what user journeys could we extract from these business requirements?
The first journey I can detect is when I provide an IBAN to the software, I see validation details displayed. This thing. I consider it's a critical user journey. Why? Because if it doesn't work, the purpose of our app is lost.
The user doesn't have any reason to use our app. if this main feature, main behavior or journey doesn't work. The second journey I see here is about when the IBAN that I, as a customer, provided is not valid and therefore is supposed to see an error in some sort.
Here the UI describing it. And I consider this is a minor user journey to display that error because look. If we fail to deliver the error when I entered an invalid IBAN into the input field, I as a customer will recognize like, okay, maybe something wrong.
Maybe this IBAN is not valid and therefore I don't see neither results nor is there an error message. So this is my decision that I take that, okay, this second journey is more minor, more like an edge case. But the first journey to output the results is actually the critical.
Important is to identify these journeys when you look at your business requirements, because they will help you to think what kind of test to employ. And here I have a simple diagram. Whenever I identify user journey, I try to decide what test fits best.
The user journey that is critical, I think, shall work no matter what. The best test from perspective of confidence would be an end-to-end test because it replicates the user environment almost one-to-one. The testing robot would open our app in the browser just like user would do and go through the steps the user, the customer, goes when completing a certain journey. For the minor user journey, I could employ an integration test.
I will explain this and demonstrate it a bit later. But I think it fits best because it's 10 times faster than end-to-end test. It won't communicate to any API, nothing. And therefore, I'm fine to validate like edge cases using that kind of test.
But only edge cases that could be represented in a user journey. And last thing, if I see that in my... application development, there is a business logic involved that is not directly representing a user journey, but only accompany it.
Then I will pick a unit test, the fastest test that we have available. All right, the first thing what I try to do is to describe our critical user journey. in a form of a test. And this is a Gherkin syntax. You probably are familiar with these given, when, then keywords.
So I'm trying to, in a human English language, explain the journey that I want to develop. Given I open a validation page, when I provide an IBAN, then I see validation details. These... test even it doesn't it's not a test at the moment it's just a in english written scenario it actually will serve us as a high level documentation for the whole team not only for technical people for example when onboarding new developers or new people in the team could be business analysts designers and so giving them to go through this kind of high level test will create an impression very fast how the software actually works. I call this type of test an acceptance test.
And here is implementation behind each step. So I guide in the first step when I open a validation page, I guide the testing robot to visit a certain URL in my app. Then I guide the robot to find an IBAN input field by an HTML element that has particular attribute, in this case, data test. and type in this field an IBAN and then hit enter. Afterwards, I guide the testing robot to actually verify whether certain strings are displayed on my page in my app.
This implementation is done using Cypress. Cypress is a tool that helps to write this kind of tests. Now, if your environment or your infra doesn't allow to spin the api or backend to run end-to-end tests it's fine we could run end to mock tests so instead of going to real api in the step we can mock a particular api endpoint and make sure that when robot opens the page and there will be a case when our application will communicate with the network that a request will be stopped and a stop will be returned.
Okay, let's run this test now. Of course, it fails. I just put a test up front, but no production code was written. That's a red test. What is important at this stage?
is that we clearly understood while writing this high-level end-to-end test what the expected software behavior, what is the user journey we have to implement. It's time to work on implementation now. So I go to my app component, the highest component that outputs Hello World, and I replace that line with rendering validation page, a component in React.
that doesn't yet exist. That's fine. Then I create that component that I just put in my app, validation page, TypeScript file, and using my internal design system, I try to compose a view.
I try to put some title in there, already putting some form with an input field, which is my IBAN input field, and this mid button. Plus I also fix a display of our validation results at the moment like this. And we can already see even in the browser that the output will be like this because of my internal design system. Okay, now the next step what I do, and it's, I believe, familiar for many React developers, is to introduce an internal state to make the form controlled.
Plus, I add onChangeHandler to my IBAN input. Whenever the customer types a value inside the input field, I want to store that value into my internal state of the component using useStateHook from React. In the next stage, I think already, all right, so my form has to be submitted.
And when the form is submitted, I actually have to do an API request. I'm aware of that because of business requirement and API specification I have here at the right. And first thing what I do, I type a validation response according to this response table in my API specification.
I identify IBAN, FLACS and Trust Core of the bank. And at the left side, I also think, okay, but I will make this API request, but where I'm going to store the response? And in React application, you use some kind of state management.
In my case here, I'm using React query. It's a popular state management these days. And I'm importing this use query hook from React query by feeding a validation response type from TypeScript in that, so React query knows about this.
Plus, I introduced here a submit handler. So whenever the form is submitted through the submit button or by hitting an enter key, I want to store the IBAN at that moment in some internal state and feed this IBAN to my query. But to make sure that the query can actually store the response, we need to make an API request. In terminology of React query, the query needs a function that will do an API request. And look!
Currently, we were composing our view, just putting some code executed in a sequence. We employed some state, we rendered some HTML and React components there, but now we come to this moment where we need business logic. So what is business logic? I think it's wise to review. Developers building React apps often refer to the term business logic, and most of the time being uncertain what it actually is.
In reality, it's that simple. Business logic are if statements, logical control structures, for example, if else or switch loops like while, for, for each, for in, for off, data transformations. data extractions, retrieval of the data from some place, and side effects.
For example, API calls. I also consider the business logic. So business logic is purely a technical term invented by programmers. Don't mess up it with domain logic. That's another thing.
All right, we are back to our component. And now I paste it here at the right side of the screenshot. from React query documentation and they visualize how to employ global fetch function that browsers provide to fetch some data and then these data will be automatically stored in my React application store that the React query will do.
So our idea is to build such functions like in this example but what shape it would be if in their case they uses to do ID, but in our case will be Ivan and I think the shape of that function Shall be something like this It's a function in our case that will create another function So a factory that returns another sync function similar like here at the end But we need another one a wrapper one because we need within a closure JavaScript closure to store the Ivan and then access it here when we will be doing our API request. So I picture it like this, but that's just my imagination. So what we start with whenever we face the business logic demand while composing our React, we start with a unit test. Here, I'm created a validation API service test. In this test, I'm trying to assert.
a successful response for provided IBAN. I start writing tests from the assertion because it gives me clarity on what behavior shall the function do. In this case, some result shall be equal to the output that looks like a shape of my API.
Then I continue with an action and here I'm trying to think of a way to do it. how I build this kind of shape that I put there in my picture. And I think we need a function which we will let's call create IBAN validation API adapter that's a function like this that will return and retrieve an IBAN as an argument and will return back another function in this case like I picture it as an adapter.
And this function will be asynchronous and it's on its own will finally do these global fetches executions to communicate with an API. And when I call this function I expect to get the result finally the API response which I assert there. All right and the last thing that I do when I write unit tests I do arrangement. So you have noticed that I did wrote a test backward like Assert, Act, Arrange, AAA pattern in favor of clarity. This helps you with clarity.
Okay, so we have this test and now we can think more. But okay, this test doesn't do any API request, nothing. So this is a trick now. Instead of mocking any global fetch function, I don't want to go neither to the net. because imagine how slow will be unit test if they go to the network.
I want to somehow fake the global fetch function, but I also want to make sure that my test is not coupled to the inner detail. My test is not aware that there is a fetch used. So here is what I come up with.
I create a fake using VTest tooling that could output a similar response that fetch can do. It's like status okay and some JSON method that will return you here a fake API response. Then I enhanced my function which yet doesn't exist with a second parameter which will be options where I pass request property as my as my fake, as my spy. And then at the end I want to also to assert some details that actually my fake that I created here will be called with certain API endpoint.
And since we are supporting REST JSON responses, we have to provide a header to get back a JSON response. All right, this is just a test, no production code in place. Now let's work on the production code to make this test pass because this test will fail. We don't have any implementation yet.
Neither this function is created. So I'm creating that function itself with a signature that I already identified in my test here. So it's a function that returns another function.
And now I will start vtest in a watch mode and obviously my vtest unit test will fail. dramatically because well there is just a comment no implementation okay so we continue by trying to figure out the minimum amount of code in favor to make this test pass so we wrote here this thing we created a variable called response and called the request which comes from our options if it not provided it will be using some fetch and it does the API request but in the in the test it actually won't do API request because I faked it I pass here the fake as a request in so fetch default value won't be taking place in production it might be taking place instead of any fix so we continue and we rerun our vtest and it passes now because the implementation does exactly what the test expects it to do. Alright, so we continue with the next small test. That test will be to assert a situation where the API actually fails. And for that we try to create a fake of our fetch mechanism that connects, that interacts with the network.
And the OK status is false here and the JSON function returns some string, some error, let's say, that API could theoretically provide. The arrangement here stays the same. So the mock is in place, or in this case it's mock, in another case it's fake.
And here is an assertion that we test allows use when we call that adapter. it should be rejected. So it shall throw an error like this. We run our test, of course it passes because we don't have this logic implemented. And we continue now with implementation.
And here we added some more details of the implementation, some if condition, if status is okay. Then we return the resolution of JSON function. Otherwise we throw the resolution of JSON function.
And we rerun the test and the test now fails because it now considered when status is okay false then well it throws as a per implementation and here we assert that the error is actually thrown. When having green tests we are now safe to do some refactoring if necessary. And here I'm introducing an intermediate variable to resolve API response in advance. Okay, so it's a small refactoring step. When I rerun my test, my tests are green again, so I'm confident that I didn't break expected small behavior of the unit.
That's quite good. You might not realize, but actually deciding to develop the API adapter aside from the React code impacts our application design and architecture. The adapter we developed is now an isolated functional layer and the test played a significant role to have it isolated.
All right, so what we actually were doing now is we were writing a failing unit test, small one. Then I tried to come up with some minimum amount of code to make it pass. Then I was repeating with the next failing test or I did a small refactoring if it was necessary, but only when my tests were green. So I was confident that changes that I do by refactoring doesn't break the behavior. We iterate it through this circle.
This circle is called Test Driven Development Circle, where we always prepare a test upfront, then try to make it pass, then fine-tune our implementation. Iteration after iteration and we end up with isolated API adapter ready. And it's ready to be consumed.
Back to my validation page component, my React code. I import my API adapter directly into my React component and feed it to the React Query function. Now it's very similar to how React Query docs shows working with fetch. And since I'm using React Query, I have to wrap my validation page or the high order component that we have in application with query provider in favor react query can manage the global state of my react app so i'm doing now while composing the view we arrived at the point that we need more business logic we need an if condition in react's context here at the bottom it will be conditional rendering the first question I asked myself again how do I test this and you already guessed that I will put this aside and build this using unit test the business logic so I put some valid some some unit test that I want to return the whole view model or in other words the object that my view or React component needs to render properly. The bigger the unit, the better.
So I decided to create one function that will process the data from API and do stuff and outputs an object that is used only by the view and necessary by the view. So it's a kind of factory that will produce a view model. This is what I want to build.
So I start with an assertion again. I expect that some result, we don't know yet what, will have a property isValidationAvailable to be false when, and we call this availability, when validation, the response from the API is actually not available. We continue with an action and we picture that there will be some create type and validation view model function.
It doesn't exist yet. I'm just trying to draft it in the test. my design and naming in the test. And then we end up with an arrangement where we arrange that input validation that's response from API is undefined.
Very simple test that asserts just one property from my pictured view model, this result object. And I continue, I introduce the validation view model service and now finally declare. the function with its signature as per my test. There will be some validation that's response from the API.
And to make these tests pass, I just put false. That's it. I run my V test and the test passes. All good.
Then I continue with the next small step forward and write another red test. When availability, like I want to assert that this is validation available, availability is true, then validation, when the data from API provides some results, for example, it provides IBAN and flux as per interface of the response from API. The same action, and now I rerun the test. The test obviously fails because the production code always returns false.
So how to make it pass, actually both tests pass? We need to introduce some business logic, a Boolean that will convert our validation response into true or false. and we rerun the test as a test pass all good and i think it's now the step that we can integrate this small condition already in our view so look i added this line where i created a model variable by calling that function that we just designed and feeding the data from react query the data that actually api adapter provides in the react query allocates within our React app state into my factory method here.
And I allowed myself to also put here an if condition or not if condition, conditional rendering only renders this list of items with fake data at the moment when this is validation available is presented. Okay, let's run our end-to-end test again and we are running the test. It's already in a better state but it doesn't pass still because this data is still fake.
We expect something else here. We expect, the test robot expects that there will be some valid IBAN message in place. That was programmed when I was implementing the steps behind Gherkin's scenarios. Okay, so we continue with the next thing.
And the next thing will be trying to replace that fake XYZ results of my validation to a real ones. And that's, again, a business logic I put here at the right from my task, how the things shall do, when which validation result shall output based on which criteria. And we will try now to write a test for this criteria actually. So we start with the first test where we picture that our result, so a ViewModel, will contain some validation results property which will be an array, an empty array, when the response from the API is not available. Action is the same.
And we will run our vtest. Since I'm running it in watch mode, it will constantly help me to see where I am. And of course, it fails because we don't even have this property introduced in our implementation.
So we introduce that validation results with empty array. And guess what? Our test will immediately pass. Well, because here is an empty array and this is what we expect. OK, we continue with a small next.
test and this will be now looking at the API specification and these details criteria when certain validation results shall be displayed and when not. And what we do is to expect that validation result is an array with a valid IBAN string when validation is just available. And here I'm trying to fake a or stop the response from API with IBAN and no flags and then call my action.
When I run my unit test again, the second test will fail because the current production implementation now returns only an empty array. So I need to put a minimum amount of code to make this test pass. So I put a ternary operator here.
When validation response is actually available, I just put an array with valid IBAN just to make sure this test pass, otherwise I put an empty array. So I rerun the test, the test passes. So good. We are continuing in baby steps for the farther requirement based on this criteria here.
And now we want the validation results array to contain trusted bank. In case as per criteria here the trust score is over seven so we stop our api response here with the trust score eight feed it to our factory that builds a view model as a validation variable and rerun the test and the test fails okay the test failed that's fine but look with this ternary in place i cannot extend it with a new criteria of putting trusted bank into this array so i do small refactoring step at this stage And here is what I do. I just introduced a result, empty array here, then I put an if condition, if validation is just available, then I push to this array valid iBank.
I rerun my tests and two first tests they pass, the last one still not because there is no nothing about trusted bank implemented. So okay after this refactoring which was also safe I continue to do the implementation. And now another if condition, another business logic where I see, okay, if trust score actually arrived from API and it's over seven, then I push to my results are a trusted bank. Okay. I run my tests.
They are now again green. That's good. Feels like you want to refactor this multi-line if condition and abstract magic numbers in there, right? I would do refactor too. I feel again totally safe because my tests are running in a watch mode, they are all green, so I could do some refactoring by maybe abstracting away this big if condition.
So in baby steps we will continue implementing further logic as per this criteria here in my task to imp- Implement the whole view model, the whole validation results array, so it satisfies this criteria here in baby steps as I demonstrate. And at the end, I fast forward a bit to do not show you every small baby step, I end up with this kind of code, production code, that here I abstracted away that big if condition into hasTrustedBankPrivate function somewhere above this. And I implemented the rest of the criteria in the same manner.
Red test implementation if necessary refactor. Red test implementation if necessary refactor. Many people are not aware that V-test provides you a coverage report, very detailed.
So you could run your test and it will output this kind of coverage where you could explicitly see whether each line was properly asserted and executed in your unit tests. In my case, I did a good job. I have 100% coverage. And actually, this is what we have to strive for when we build unit tests. All right, we are ready to integrate validation results, what we built in our ViewModel, into our React code.
So we are back to our validation page React. component and here at the bottom we still see this fake data and now what I do I just replace this fake data with the model.validation results which I take from that model that is generated by this factory function. So we took another decision actually when we were test driving our view model to build it aside from React and it impacted the design. We didn't overcrowded our React component, which is considered to be a view layer. For example, when at some point we move business logic into the backend, the business logic is already isolated.
I know some people from LinkedIn developers community are using BFF. It's backend from frontend. It's a kind of intermediate API that could do the business logic and feed to the React application.
already prepared view models. So in my case, I don't have this BFF, therefore I put the business logic and design it using unit test, but it's isolated and ready to be ported anytime we want it. All right. Whenever a new business requirement comes to enhance this, let's say, feature with validation, I will also probably modify only my view model code, but won't torture React.
component. I will maybe introduce a new criteria or validation. So this isolation will help you actually to maintain this code farther because you won't have to touch the React component.
And it feels like a single responsibility, no? The view has its own responsibility to deliver the view model to the view layer. The view layer has a responsibility to render some HTML React code. And that's, yeah, that's the case. And I think we are ready actually to run our end-to-end test.
So I'm running the end-to-end test again. And now I see it's passed because we replaced that fake data with a unit we developed with our view model and software behaves as we expected. The journey is now covered with a good test, end-to-end test. So what we actually did is we introduced another SOQL in our development flow. We wrote some failing acceptance test that was our end-to-end test and we started composing react component react view whenever we faced a demand for business logic that was an api adapter and view model we immediately turn to the inner circle here a small one where we build and design a particular unit using unit tests red test green refactor if necessary repeat by repeating multiple times we end up with good isolated state of our business logic and then we try to rerun our acceptance test a big circle and it has then passed this kind of test driven development it's it's called outside in test driven development also called acceptance test driven development and it's There are multiple styles of test-driven development and this one explicitly is called Detroit School.
All right, now I think it's time to implement the second business requirement which we have is to display this error message in case API fails. And to do that, since we decided at the beginning that this is not critical user journey but rather an edge case, I decided to pick an integration test for that. And the integration test in Cypress looks like this.
It's similar to end-to-end test, but there is no gherkin involved. So what I'm doing is I'm asserting that this IBAN is invalid, that's an error message, should be visible on the screen. So I'm guiding the testing robot to make sure that it is there. And also I'm guiding the testing robot to type certain IBAN, invalid one, inside the IBAN input field and hit the submit button.
And there I somehow don't want again that my test interact with the network because this will be then slow test. This is not an end-to-end test. This I need to be very fast. Therefore, one way is to fake API request actually. And this could be done in React's context using inversion of control.
And for that I introducing some context provider. That's a simple tool link that I have in my repo, you could see afterwards, that helps us to achieve a type safe React context API. Okay, so the syntax is like this.
I defined for that case an interfaces. Here we can see familiar already things. This is API adapter.
That's a function that returns you validation response asynchronously. And that was a factory that we built designed with unit tests, which supports IBAN as a parameter and outputs API adapter. Now I configure my React context using these typings in favor to make sure that in TypeScript I have type safe context.
And I create also a public use validation adapter factory method that takes the value from react context api and just returns it back and now i can employ this thing this additional tooling that i had by creating a fake api adapter that will match this type that i have here and the one that we designed with unit test And in this test particular, I wanted my API adapter actually throws an error. So I pass this API adapter in a form of factory function, similar to this one. Here I omit the iban parameter because it's not necessary for this certain test here. And I created this fake API adapter.
But okay, test is test. And of course, if we run this test, it will fail because we don't have... the dependency that we pass through these tests, like this fake implementation of API adapter, we don't use it anywhere. So the test fails with some error, probably because something is not used.
And now look what I'm doing. I'm back to our validation page component and remember we here imported directly our API adapter. So we tightly coupled our API adapter to the React component.
I don't want this anymore in favor to have a test that works and is resilient. So I remove the tight coupling and I import this hook from my context thing and its hook just provides me an adapter and I call it adapter. Now what happened? Actually now we made sure that dependency of the adapter is inverted and the validation page is not aware about the implementation of the adapter. It's only aware of the interface of the adapter.
Okay this is dependency inversion principle here in place. We are loosely coupled now. between API adapter and the React component. All right, since we had put some fake in real production code we don't have to put a fake of our adapter like in test we did, but we have to wrap like in test with this context provider, which is type safe. and feed the real implementation.
Here we import a real implementation of the adapter and feed it to there, so it's stored in React's context. And now every consumer of React context, which will be consumed from there, will actually receive this implementation. But as you see, this code is very similar to our test, and therefore in test we faked it and made sure that API adapter is not real and it doesn't go to...
API event. Okay, so this was an integration test and beyond that I continue working because I need to make sure that the error from the API is actually delivered to my React component. And I take the error from React query because the React query provides it when query function actually throws and in my test I made it throw. And now I picture that this could be a second argument in my view model.
Even type script will fail here because my current signature doesn't allow this. Plus I employ some prop from form field design system in my case and feed the model.validation error inside, consuming that there will be some validation error. And it's not yet defined.
I just define it here while composing, but it's not defined. Type script will throw an error here too. I'm using property which is unknown yet. So business logic again, we enhance our or extend our view model a bit and here we pass inside the view model function that generates the view model an error like I just put in the composition and expect that IBAN is invalid.
So a message is received and when I run the test, the test is failed because there is no second argument in the signature. so we go to the implementation here and enhance the implementation with the second parameter and to make this test pass we introduce a validation error variable just by hard coding here the output that we want to see and put it there and there whenever we return from this function and of course this test will pass yeah but we don't want that it always outputs this error we want to this error is outputted only when API actually fails and the error is provided here. We don't care about the error type at that moment, only that it is error or no error.
And therefore, we create a small unit test again, enhancing our model by putting the error which is undefined inside as a second parameter, expect that the validation error will be undefined here. The test immediately fails because at the moment validation error always comes as a string. So we put some business logic in this case ternary operator when error is available then this iban becomes invalid this string like is outputted like this otherwise undefined. Our test fails and actually our test passes and actually we are ready to try to run our integration test already. You could also refactor this as a safe state when everything is green.
In this case, we can abstract, for example, retrieving the validation error into some private function. Okay, and now I think, yeah, we arrange everything for our integration test to pass. This is an integration test where the similar code is to production, but instead of real implementation, we put this fake, it throws an error. And let's try to run our integration test.
It's similar to end-to-end test, but it's much faster. and here we see the output visually that it passes now because we implemented the logic for it to pass. That's awesome.
And look what we now introduced some kind of dependency injection or dependency inversion here where we invert the dependency of API adapter and then consume it in our production code. This allows us to fake it in the test. and feed a real implementation of adapter in production code.
We had dozens of discussions with my team about this. And also I discussed this with folks on LinkedIn about this inverting of API adapter dependency, whether it's really worth it to have a loose coupling here between these two functional layers. And you know, within our team, we came up to the conclusion that it's an overhead.
I just showed you an overhead, I think. Especially because in our case, in let's say we have some pages that consumes around six API endpoints. So in my integration tests, I would have a lot of fakes for different API endpoints here. So we decided that, okay, low coupling has its benefits, but being aware of what API is used for a particular page gives us more documentation, gives us more information, and gives us more value.
Therefore, we dropped faking and dependency inversion here and introduced just mocking. Cypress allows this CI intercept method to mock a certain API request and make sure that the network is not reached, but the stop or how we want the request to respond. actually responds.
So we cut it short like this instead of overhead with dependency inversion. Okay, I will run high order tests, end-to-end tests in case we broke something but it seems we didn't and you see it run and execute it and asserted that everything is sprayed correctly. Our initial usage journey just to make sure everything works and it's green.
I also run all the unit tests and they are green too. This is good. But we develop them like in baby steps they of course are green.
We can also at this stage can try to play with our React app by hand now. So do a kind of manual test if we want. Altogether we build quite a solid safety net. And here I try to visualize this safety net that we build using testing pyramid.
The first layer of testing is like manual, explanatory test. That's very accurate, but it's the most expensive. It requires human to do clicking on your app and validating the things, especially in enterprise grade React applications where we build a lot of features and our application is huge.
Like manual testing is a big investment. Therefore, the second Test which is super confident is end-to-end or end-to-mock test. Normally we employ it to test critical user journeys. It's slow because it boots the whole browser and try to replicate the user environment, actual environment one-to-one. So we have to be careful which tests we put in there and which not.
The next layer is integration testing. We are using Cypress components testing by asserting a journeys happening on a single page and these are edge case journeys, minor usage journeys that are not critical to our business because the critical parts of our business in our case in digital banking application critical is to enable people to make different type of payments domestically. payment, setup payment, international payment, international instant payment.
These are all critical user journeys. Or maybe when you go to your banking app to copy an IBAN to share it, that's quite also a critical user journey for us because people do it a lot. All right, but the integration tests we use for minor user journeys, they are 10 times faster than these end-to-end tests.
and therefore we employ it. And all the business logic we use, we handle using unit tests because they are the fastest, the cheapest, they can run in a few minutes, thousands of tests could be executed and evaluated. So back to our test-driven development outside in Circle.
And to implement invalid IBAN, we also created an acceptance test. but using integration tests in this case, not an end-to-end test. Then we came up to a business logic to output the error from our view model. So we extended our view model in baby steps.
We created a red test, then made it pass, and another red test, then made it pass, and refactored. And at the end, we ran our integration test to make sure that it passes. Important here is that this Detroit school and outside in test-driven development helps you to come up with tests which are very silent to refactoring and therefore we can do aggressive refactoring without carrying the tests will break if we really refactor don't change the implementation details for example here the comp the react component is doing too much i think beyond just rendering it's also take care of the state. So I want to do some refactoring quickly. I will abstract the repository away.
So I created a use iban hook here with use query in place and that's actually the code that I just moved from there to there and now I have an isolated repository functional layer in place. I also could abstract away the hook that does support the journey. actually this internal state of the form and the usage of repository here and the usage of the factory function that outputs a model.
This I can abstract to and you see that our view became also very very thin. It just does one thing. It renders this the the html output or react output and it's aware only of its view model. I can also abstract an IBAN input from this setup into some custom React component here, just a way to make sure my view is super neat and super self-explanatory.
Knows it says what it does and knows only about a very limited information, the view model. So we did aggressive refactoring, quickly running end-to-end tests, it passes quickly running integration test, it passes. Of course, our unit tests are passes. We did some inner code structure changes, but no maintenance was required for the test because tests are resilient.
The goal actually is to come up to the test that doesn't bother you when you change the inner structure of your code, but rather bothers you only when the software behavior changes. And these are functional layers that we end up with a view. that only renders that use case. It orchestrates everything for our user journey and provides a model to the view. The repository where we employ React query that interacts with the state of the React app.
The adapter that interacts with the network. We build it in isolation completely using a unit test at the beginning. And the service layer that handles all business logic.
When the application is sliced this way. It's easier to maintain it, to port every functional layer away, to reuse it, and even to replace. For example, if you want to replace global fetch method in your adapter into some Axios, you are fine to just modify this functional layer.
If you want to modify the repository layer to replace React query into some MobX, maybe state management in your React application, you will modify just this functional layer. Less things to modify, better, easier to change. And now a few words about zero-bugs policy.
That doesn't mean you don't have bugs. It just means the type of bugs you will discover will be different. Normally it will be edge cases which the team were not able to catch yearly and implement in a form of tests. Or this won't be bugs but enhancements that people will claim these are bugs. And actually zero-bugs policy means you have a system.
to ship a software that works. And here is a system that I introduced, want to introduce you, a routine that we in my team employee. First we test drive our development, try to do it together in a form of peer remote programming sessions.
We jump to write tests together with test engineers or business analysts sometimes. Then we jump to me peer programming session with my peer developers and try to develop this stuff. And we get feedback from all end-to-end tests every morning because they run nightly.
We cannot afford them to run every pull request because they take quite long and here for example is an output we get in slack when the test broke or when the test is off when the end-to-end tests are okay so critical user journeys works or not whenever they break like in this case to test case we prioritize to fix them as soon as possible in the morning so we investigate why they failed and so forth and we try to work in a small request i also suggest it to everybody but frequent pull requests. Kind of trunk-based Git workflow. You could check online what it is if you don't know.
And we run all integration and unit tests on every pull request. So here is an output of our GitHub actions here. Whenever you push it, change it, there's a pull request and to end the unit and integration test will be executed and will not let you know immediately if you broke something else with your code, with your new code or not.
Maybe continuous delivery in this setup could be employed. We don't do this. For us, it's just continuous integration in this kind of shape.
All right, to end my session with, I wanted to give some benefits of what test-driven development actually brings for me and my team. First is quality. So I know what is expected from me to build because I write tests up front. Therefore, I...
nail down the expectation from a smaller or bigger software behavior up front. Test-driven development helped me to end up with a simple design, separation of concerts. If necessary, since everything is isolated, I can easily make loose coupling by inverting dependencies if I need and want.
I end up with a safety net in the form of tests, tests that are resilient to ref- factoring of inner code structure. This gives me confidence to do inner changes because tests always tell me. Some tests like end-to-end tests are leaving documentation useful for the whole team, not only for technical people. And I get quick feedback from continuous integration tests that I run on every pull request so I could quickly fix.
And most important, I get a clear metric whether my team delivers. quality software or no and this matrix is not just i think that i deliver something good but really test says if they are green then everything is good you deliver the quality this gives me confidence shipping react up like with with good quality all right so i suggest to figure out the designs of the behavior beforehand alone in solo mode or together with your peers like test driving development with your team by pay programming or more programming. Managers could claim that's not effective, but believe me, I'm practicing the pair and more programming almost for a few years already, and I feel so much benefit for the team and for the quality of the output we produce.
Try to set up a system that can allow you to fail yearly and fix this up because you will be aware. where is it broken. Know your quality metrics. For me it's just green test here and there, doesn't broke anything. Ship with confidence on green and with this setup, with this system, you could live a good life.
Don't stress much. Now we know another approach to building software. Keep building on hacks or build well using test driven development.
All right, so my name is Nick and I'm a big fan of test driven development. refactoring and clean code. I build payment systems at Airsta Digital. If you want to get better at React development, there are different ways how we can cooperate. For teams at enterprise grade similar to mine who wants to learn shipping quality software, I have a workshop you can reach out to me.
Otherwise, almost every day I post a bite-sized training lesson on LinkedIn. so together we can learn, discuss things in comments. If you want to work with me personally or in my team to practice these extreme programming things like test-driven development, pay programming and so forth, click on my profile on LinkedIn and there is work experience section. You will find Airstate Digital as a company I work with. Click on their name and you will land to their LinkedIn page and there is a jobs tab with our current openings.
We operate in Central Europe, in Austria, Slovakia, Czech Republic. We work on-site and in hybrid mode. If you find that I can bring value to your team, you can invite me to your company to speak at your breakfast or some event you have, or at your conference or podcast, whatever you want.
I also offer mentoring opportunities for people who want to become better. My contacts are available at my website, weisnick.com. And yeah, that's my story for today.
Thank you very much. Thank you, Nick. It was very interesting. I think I need to do this.
So now you can actually see us again. So let's get started on the questions. So the first question that actually came up.
was well could you share the repo so we can follow along and this one came another time as well and could you please share the repository at the end of the video um can you absolutely there is a repo my repo great so on youtube yeah sure perfect can just post it here so so when we go um to the questions so let's have a look at this one so sometime we use react context to hold all states used within the app but end up with a large number of states and difficult to maintain these huge states? What are your suggestions for such scenarios? Yeah, good question.
Actually, React context is fine for React state, for the application state to manage it, to keep it. But at a certain level, as soon as your application grows, I think using the React query or other state management just super simplifies it. You don't have to care about much boiler. plate code and where your data is allocated because React Query manages all. And there are these query keys where you can access the data from any component, for example.
So I would say that it depends on your app. If you have a small app, like my example today, then React Context would fit in well. Otherwise, whenever you go, pick some better React Step Management. And there are multiple, Mobex, Redux, React Query.
Okay, great. So then the next question is, shouldn't you use use effect with react query? Or is it normal to have normal objects in the air when using react? With react query, you don't need to use use effect at all. That react query managed for you automatically, it will automatically execute the API adapter, which you just feed it as a function.
and will make sure that it outputs the data where an error or is pending state when the API is taking time. So that's the beauty of this additional library. Great, so already answered that.
Next one would be, I think MobX really is good for extracting concerns to adhere separation of concern as much as possible. So what's your perspective on this? I haven't used MobX before. Redux, yes. React query, yes.
Redux query too, but MobX, no. I know that it helps with preventing mutation out of your state. So I think I'm not the best to answer the question in this library. Sorry. Okay.
No problem. Then we have a Technical question to Nick. When we are using global state management tools, we are dealing with a global variable.
When tests are following each other, one test result is affecting... One... Another. One test result is affecting one another. One another.
When tests are following each other, one test result is affecting one another. Ha! And now comes the second one, sorry.
How to deal with this kind of stuff? Do we also have to mock dispatch functions? No, no, we have to design our code in such a way that we don't have to mock much.
maximum what we can mock is that I have shown in integration test is a very high level of our interaction with API. So we mock just one functional layer, which is API, but the rest shall work as is because at the end, we want that our test, integration test, for example, assert that everything is wired together correctly. So therefore, mocking shall be, if done at all, then... on the highest level possible, but the rest of the application shouldn't be anyhow mocked, but executed as is the production code.
Yeah, and that's how you described it. And here I also want to add an additional point. This could also be perhaps related to sociable versus solitary unit testing approach. So for example, here what you've illustrated.
For example, mocking, minimizing amount of mocking and mocking like, you know, when we get to the extreme boundaries is essentially the searchable unit testing approach, which is also something that I practice as well most of the time, where essentially mocking is minimized, whereas people who may choose to solitary unit testing approach, that's where there's a lot of mocking. but then that's where also that the problem of the test fragility so do you have a general like guideline when to mock and when not i mean you said don't don't mock at all fake yeah ideally don't mock at all fake but as i said in our experience that we decided that in our case when we consume around six or even more API endpoints for one page, it becomes just over overhead to provide so many fakes somewhere at the top of the test, not only in the top of the test, but also in production code, not fakes, but real implementations. Therefore we decided, okay, it's easier now to mock the top boundary for API endpoint there and don't overhead with dependency inversion. I have a follow-up question there as well. So is this the case where the page is independently reaching out to six API endpoints, that it's aware of the API endpoints, or is it the case where you have an API and then just behind the scenes it is...
connecting to six endpoints and then consolidating. I got your point. In our case we are consuming APIs that are also consumed by other devices. For example, bank commas are consuming the same APIs like we consume. The one where you take cash from.
The software that is used by bank consultants are using the same APIs for different stuff. and many of our mobile app is using the same API and so forth. And therefore there are so many different APIs that we have to put together and we don't have any BFF back-end for front-end, this intermediate layer that could aggregate all these and feed the React app only with one endpoint that will provide the whole view models or something like this. Therefore there are so many complexity on the front-end therefore for us because we consume different APIs that other consumers consume too.
Great, thanks for that. I just asked myself wouldn't it make sense to have an abstraction for all these API calls and then use this abstraction instead? I mean that you don't have to mock all these API calls but have like one that they come... accumulates all the data and just gives you the data back and have an interface for that again. Well, that's BFF, backend for frontend pattern, what you are talking about.
In the frontend, basically. Yeah, that would be cool. That would simplify our single page application.
Yes. So you also don't actually create backend for frontend in that case. You want to do it in the frontend.
What is the reason that you don't have a BFF? I mean, I'm quite used to BFFs actually. Yeah.
I think the reason at the moment is that it's like from the historical point of view, we grew our app by building a single page and handling everything on the front end. And we hired many front end developers to do this. And then we reached the point that there is so much complexity. And what we do really hire more back end developers who will develop for us this BFF layer, or still we can handle this on the front end.
And so far we can. with our super core team that provides us infrastructure tooling. We can do this without a problem at the moment.
But yeah, it impacts, I would say, our work. So in the future, it might change. It might change.
Great. So we also have another question, a small one. Do you ever test rendering with tools like React Testing? Well, that's a good question.
Actually, React testing library is the first one that we evaluated for integration tests. But the output it produces is just a random HTML code inside the console of your terminal. And since we have a design system in place, which we are not aware of, it outputs so much HTML code that it's not...
possible to understand at which wrong logical branch you ended when your test failed. And we found out that Cypress Components testing, or there is another competitor called Playwright from Microsoft, that could visually display for you where you end up in your test. And whenever you end up with some wrong logical branch, you visually see, okay, I end up here.
And therefore, I can understand quicker. where I made the mistake and fix it. So have you tried Playwright also? Or why did you come to the conclusion to use Cypress? We didn't try Playwright much.
I just played myself in some demo projects a bit. But Cypress came also for historical reasons because we started with end-to-end tests already many years ago. And Cypress was the first on the market that offered let's say, comfortable end-to-end testing for UI. Yeah. And now it introduces components testing and we like thought, okay, components testing is actually could be our integration layer because they are much faster than end-to-end tests.
Yeah. And you're using the same library, which is also a benefit, right? Don't have to have different libraries.
Yeah. The syntax is the same, very easy for test engineers who also maintain some end-to-end tests together and for everybody it's very familiar since it's consumed for multiple years already. Yeah. Okay.
Do you guys have some questions? I think I see a few more other interesting questions. Oh yeah, they came up.
Wait a second. They came up right now. Yes.
Yeah. Okay. So, I will start with that one here. So I hope it's a question. I haven't read it yet.
Hi, Nick. Thank you for this session. I'm new to TDD, recently started learning to write unit testing in chest and Rack testing library. So I would like to know what resources or what approach should I follow to learn end to end and integration test unit testing and how should I. practice perfect question follow me on linkedin let's team up for mentoring and uh together we'll come up to a grow yours and mine will be fun i have a question there uh as well so and i must say your LinkedIn posts are definitely very insightful and even appear on my feed, I think, probably nearly every day.
So that's good. Aside from that, let's say if someone is interested in also maybe reading books. or tutorials or courses, is there anything, especially combining TDD and like frontend, like React or something like that, that you would recommend? You know, I'm reading books too and articles, but I'm reading backend books and then project the backend ideas to the frontend.
I think that's the best and I could recommend, of course, Test Driven Development book by Kent Back. I could recommend Clean Code by... Until Bob, I could recommend unit testing by Vladimir Korikov. These are beautiful books that will help you to understand how to build a super software.
And of course, the tips that they provide, even with different syntaxes sometimes, it will fit the front end a lot. Or maybe refactoring, first book or second book by Martin Fowler. This is a beautiful book too that is applicable for our work.
So. I don't see that we need to focus on specific React cases and React books. I think the overall software craftsmanship books are the best. Yeah, and I also see a bit of clean architecture in that way you structured your code.
Is that correct? That's a big topic we could tackle next time. I'm trying to kind of take ideas from the hexagonal clean architecture and project it to react but I know that many some people on LinkedIn they do this hundred percent like by the book but my team and I didn't feel these hundred percent match fits and is practical and therefore In these regards, my team is violating some concepts from the book about clean architecture or onion or hexagonal. And we are coupling some stuff together.
We allow this ourselves, you know. But still, we try to have this separation of concerns and so. But yeah, we could arrange some meetup in the future about this, but not about test-driven development. Great. Should we still have a...
Oh yeah, yeah, we have one here. A few more questions, I see. So, and what... I think it refers to a question before, and what about when global state was changed in previous test, and in the current test we want to check another functionality, but now we have to deal with new global state.
So the initial state is change. Well, tough question, but I'm aware that in React Query, you are not aware of any global state. It is manageable by React Query on your own, but you as a consumer of a certain component, you consume just a piece of that state. And if you do this, you could fake it in your integration test, just this state that is necessary for your component at that moment of time. So.
I think actually global state in React, I think is a very problematic thing in our industry because many tooling and many concepts are competing with each other. And I believe, and my company also believes, that we shall stick to simpler tools in favor of developer experience. This will simplify testing this will simplify the development and also management of the state great so let's have another question the last one for today and it also refers to another question i think so if i understand the api endpoints are third party and not code you control is that correct yeah api endpoints are third parties api The call to the API is a business logic, and this is not what we control.
And therefore, we have various options how to overcome this. At the beginning of the session, you remember, we designed in isolation the API adapter, making sure that the test doesn't actually interact with the network, because we faked the global fetch method. So every third party were called side effects in this computer science terminology. should be abstracted away and designed in such a way that it's very easy to make sure they are not going away from your application, not going in the network, not going to some external library or whatever.
So you could fake them. Okay. Any other comments?
else. I think we have reached the end. So thank you, Nick, once again. I take so much with me from this session. Also for my test design and everything.
And also this great thinking, okay, how should I test? What should I test? Is it a critical user journey?
Everything like that was a very great session. Thank you for that. And good to be there.
with you. Thank you very much for inviting. And I hope developers community get a lesson from that and build software that's the quality.
Yeah, I hope that too. So if you want to learn how to deliver quality software faster, keep following us and see you in the next session. Have a nice evening and bye-bye.
Goodbye.