Improve the development process of React applications applying BDD & TDD methodologies. These testing methodologies make you be confident when refactoring a codebase and improving existing code while avoiding side effects and shipping faster.
BDD & TDD in React
Transcription
Hello everybody, my name is Laura Beatriz. I work as a software engineer at YOD and today I would like to talk about a pretty special subject for me and that really changed the way that I write my tests in react application which is tdd and BDD, specifically react. So before starting, I would like to introduce a little bit about myself. I'm based in Brazil in a beautiful city called Fortunapolis and the technology that I'm currently learning on and that I'm currently working with is mainly typescript, react, node.js, javascript. I'm learning a lot of things about Python in order to dive in some devops things, although I'm also really passionate about Elixir, so a lot of tools to learn. Regarding to personal things, I'm really passionate about Harry Potter and I love to play violin. If you want to chat about any of these things, feel free to reach me out on Twitter or send me a message on LinkedIn, I would be really happy to answer all of you. And if you want to follow my project, see what I'm currently working on, feel free to follow me or just see what I'm doing on GitHub. Before starting to dive into the topic, it's really important to see the summary of today's talk in order for you to gather some expectation and also for you to grab a cup of coffee before we really start to talk about it here. So the first topic, it's just for us to go back to time in order to see where we are in the timeline from the testing queues along all of the years. So along the years, we start to get better and better in testing queues and really improve it a lot the way that we write tests in react application nowadays. testing practice overview. I will explain what is CDD and BDD in an overview, not in depth, for people that are watching the talk right now and are not familiar about it. Why do you use testing practice in react? Why do you use testing practice in react? And the daily flow of developing a feature with CDD and BDD. Some disclaimers before starting. testing is fundamentally related to software maintainability. So I don't want here to be explicitly and try to force a point that tests are like just work if you apply testing practices like CDD and BDD. The point is that this testing practice has a lot of benefits. It can improve much more confidence in our mindset in shipping software. But even if you're writing tests without following these methodologies, the maintainability of your software is going to be higher than without having tests. But we also have to be aware of the trade-offs of the testing practices. So we need to understand what is CDD, what is BDD to see if it makes sense for the project that we are currently working on for my team, etc. So the majority of concepts also mentioned in this talk are not specifically tied to react, but they can also be applied in other ecosystems. So if you want to gather something that we are going to talk about here and try to apply it to a different library, feel free to do it. Because there are some things here that are specifically tied to software engineering testing itself and not so much about react. We're going to shift our mindset to adjust that to react. Now I've tried to design a testing tool timeline. It's not that good. I've tried my best. But in order for us to understand where we are nowadays in a point of testing tools quality and how these testing tools along the years changed the way that we write software nowadays. So as you can see, in 2011, even before when react was released, we had Mocha. It was being used for the majority of the community as a test runner. The thing is that Mocha was really good. And it's still really good. It's still being used in a lot of projects for a lot of engineers. But the point is that it indeed has an overhead when trying to configure it because it provides a lot of flexibility on the way that you're going to make your assertions, depending on the test style that you're going to use. When react was first released in 2013, we didn't have a certain queue in order to hanger a component and make assertions about a certain output. So we were still waiting about what would come in the future. In 2014, Jazz came out, released by Facebook also. And it really shifted the whole community to use it because it introduced a test runner style that we didn't have to configure a lot of things in the beginning. And it really helped a lot to reduce the overhead of configuration. Even nowadays, Create react app already ships a package with Jazz installed and some configurations. So it really proves that we installed, we created react application, and we are able to have a test runner already configured. cypress also was released in the same year of Jazz, which brought a lot of benefits for the test community in general, not just react, but the JS test community in general. We started to get all the benefits of doing end-to-end tests and iteration tests without having a lot of pain due to slow CIs and a lot of things. And along the years, also nowadays, cypress is getting bigger and bigger as an organization that has one of the main goals as improving the way that tests are written in software. So another important point here is that in 2016, Enzyme was released by Airbnb. An amazing job. I think that it was one of the main points of our community in general because every react engineer that started to write tests started, I think, with Enzyme. I started with Enzyme and it was amazing for me to learn how to write tests with Enzyme. And it really gave me more confidence to ship software. But along the years, I started to see some issues. I started to notice that some of my tests were getting almost unattainable in a way that a new engineer that wasn't that familiar with the tool was really hard to understand the tests when trying to read it because it was not semantic in a way of what would the user do in the UI, like click, hanker a certain component. The abstraction was much more related to the implementation features of some components. And although we had a lot of benefits with the tool, we started to see the tradeoffs of it. In 2019, the community almost, like the whole community, I think, shipped to use react testing Library. An amazing library that was released and it changed the way that we as react engineers write our test cases because now we are able to write test cases that resemble user actions and this gives us much more confidence when shipping software. For you that don't know what is tdd, tdd resembles for testing driven development. It is a cycle based on three steps. The first one referred as read, the second one referred as read, and the last one referred as refactor. It motivates us to think on the outcome first. There is a common prejudice that it is not possible to apply tdd to the UI building process. I think that this prejudice comes from some engineers that are more familiar in code bases where you are able to do assertions in certain defined data structures and when we start to think about a separate environment showing the output of an UI, it is kind of difficult in the first place to start learning how to test it. But that's one of the main points that we see the benefits when doing UI tests in react applications because we learn how to test the actions that our users are going to do. The last step here that I would like to talk about is the refactor step. A lot of times I've noticed that even if you are following tdd, you are probably going to make the test pass in the first place, but you are going to forget about the code. It is always a continuous cycle of making your code get better and better. If you forget about the refactor step, there is a really high chance of you leaving some code that is not with the best quality at all and it is going to give maintainability issues. So, the refactor test is one of the most important steps for tdd. BDD stands for behavior-driven development. BDD can be acknowledged as a methodology and it is a branch of tdd. It really improves the communication between engineers, stakeholders to understand the correct outcome. So, as engineers, we are able to write tests that share a specific vocabulary. We are also able to know what is the definition of a certain task by writing cleaner test cases that resemble all the different user paths. For instance, if you have a user story like that one, we are going to write each test based on the user story with business vocabulary shared by all the stakeholders. This is going to make the tests focus exclusively on the business value. This is a simple picture, a simple screenshot trying to exemplify what would be the test cases based on the user story that we just saw. So, you can see that we are using the vocabulary from the user story, the vocabulary that is going to be shared with the stakeholders. There is nothing related to implementation details there. We are just seeing what the user writes, what is going to be the behavior on the UI. Why choose testing practice in react? Why use testing practice in the first place? You should be asking yourself how much value is it going to bring? The difference of me going straight to the code and doing the test later. I think that it really influences us as engineers to think about a better architecture and design of react code bases. Because we are not going straight to the code, but we are going to first think about the outcome, about the value that our software is going to bring. And then we are going to try to write the better architecture as possible to implement the code. We first think about what needs to be done and how to do it. So we are able to then ship code with more confidence. We avoid side effects and regression between components because every time that I'm writing a component, writing a test for it, I'm able to avoid regressions and another engineer is trying to change a certain line of code that can raise a big bug on a code base. And also leads to a well-defined mindset when developing components. Instead of going straight to an implementation, as I've just talked about, we are going to then know different scenarios and be prepared to understand what is the definition of the code that we've done, than having, for instance, in another sprint, if you use Agile, for instance, in another sprint, has to come back to a certain ticket and try to implement another thing related to it. When to use testing for X, I think that one of the common things that I've started to use along the years is to use CD to fix bugs. The benefits of it is that it really increases the confidence of bug fixes. It decreases bug reports of things that were fixed before. So how many times you notice an engineer fixing a bug and actually he or she forgot about a specific edge case. So this can happen. And by using tdd, you write a series of tests first, trying to understand what is the possible paths of reproducing the bug, and you make sure that these tests are going to make the bug not repeat, not be reproducible again. It also decreases side effects from refracting components that are being used in different places. The steps that I usually take to apply tdd to fixing bugs is also related about simple debugging skills in software engineering. By the way, I really recommend that book that you're seeing on the screen right now. So usually try to reproduce the bug as much as possible just to understand the behavior, all the paths in order to achieve it. Search in a code base to see where the bug lives and try to write a test with the unhappy path and think of other edge cases that could also be covered. You can write a code to fix the bug and introduce a refactor to improve maintainability. Verify the effectiveness of the tests by changing implementation details. And that way, we're going to be able to ship bug fixes with much more confidence. The second case is UI testing. What are the benefits that we get by doing UI testing? Because it really motivates us to understand what should be built and then think about how to build it. Write tests that explain to the user about how to use certain functionality. Write tests that resemble user actions and not implementation details. Create tests that resemble user actions. Implement a minimum amount of code to achieve the logic. Refactor to improve maintainability and UI quality. And update implementation details to ensure that the tests are preventing regression. So these are the steps that I usually take. A lot of the times, we start writing the tests just seeing the outcome of it. And how the user will start to interact with the interface. So when we go straight to the code in the first place, we forget about the majority of the requirements of the following user story. And we focus a lot on the code and forget about the output. So UI testing is like when we are writing the tests and explaining to the user how to use the UI. For instance, an example of bad testing practices that I have seen in react before. I've noticed a lot of test cases, test implementation details. This is a really good example, just like in the description of test cases. But what is going to happen if I change the name of the setter to setImage. Or to another thing. What is going to happen if I change the component name, if I change the state name. My test case is going to be a laggest test case. Because in the end, I've changed implementation details. And this is really bad. So in react, the only benefits, I think, not the only, I think that it's too much to express that. But the majority of the value that we're going to get from tests is really the one that we can reproduce the user actions. Another case, very common one, is like files with 200 test cases with a lot of hooks trying to reassign variables, set mocks. And this is really bad because at certain point of software lifecycle, just one test is going to break. Let's say that you needed to change a requirement. Then the engineer is going to the file and he or her just want to change one test case. But then it's needed to understand how the whole file is working in order to just refactor one test case. And that's why I think that tests are the most part of the time needed to be treated in isolation if possible. And in line as much as possible. So in order to understand how would be a daily flow of developing features, doing refactors with GDD and BDD, let's build a meme generator. So this is the following scenarios. Based on some user actions, we are going to see a UI behavior. So what happens when the user clicks on a button? A meme is going to appear randomly for each click. And also another UI behavior, we should be able to disable button if a meme is being fetched. And we should be able to show loading spinner for when a meme is being fetched. So when we start to write tests, let's first implement a component skeleton. And then we're going to write test cases as to be later fulfilled. And then define different UI states according to user paths. So always try to create a balance when thinking of happy and unhappy user paths. And this is the screenshot showing how would I apply test cases. For instance, creating as described logs according to different UI states. Success loading fader. When starting to implement tests, then we start to remove and introduce calls. And always remembering to avoid implementation details and resemble user actions as much as possible. So in the first test of the success state, we're going to render the skeleton for now. Try to find an element with a buttonhole and with the generate meme test. And expect like wait for the synchronous request happens and all of the things. And then make the assertion. Now implementing the code. Let's try to write the amount of code to achieve cases. The minimum amount of code. We're going to refactor the code to improve quality and later maintainability. And try to tweak some implementation details to see if the tests are already catching it in order to avoid test regression. We're going to then write the component. So that's the first draft. I've added some states, a handler which is going to trigger the request. In order to improve that in the third step of tdd, I'm going to rewrite the logic and extract the logic to a hook. So that's the final part of developing a feature. And now I'm going to introduce the final takeaways of this talk today. I just want to make sure to pass the message that testing practices doesn't apply to all teams and projects. We have to be aware of what is tdd and what is BDD and see if it makes sense for us as engineers. Using the right tools to speed up test implementation is really important. And we can see how much we're getting better at this along the years. Writing tests that resemble user actions enable us to ship software with much more confidence. And having the discipline to apply tests in practice is the key to achieve better results regarding architecture decisions. So that's it for today's talk. I hope you all liked the talk. If you have any feedback, feel free to reach me out on social media and send me a message there. And I'll be really happy to answer all of you if possible. Thank you for watching and have a nice day.