How to Add Component Tests to Web Apps
with Ely Lucas
What are component tests? How do they relate to e2e tests? Do they improve a11y? Ely Lucas will teach us all about them and how we can add component tests using Cypress to a React app.
Resources & Links
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Hello, everyone. And welcome to another episode of Learn with Jason. Today on the show, we've got Ely Lucas. How you doing?
ELY: Not too bad.
JASON: I'm doing great. Super happy to have youen O the show. Do you want to give us a backgrounden O who you are what you do?
ELY: Hi, I'm Ely, I work the a Cypress as a design engineering. I've been doing web development for, I don't know, 23, 24ish years. Kind of got started off with you know, my first code editor was Netscape navigator gold edition. And got into front page, and once I realized WYSIWYGs weren't the way to go, I learned coding and that stuff. Doing that kind of grew up doing that. And some of my past experience has been at Ionic. I was working on an ionic over there, as well. And, yeah, kind of various other start ups and whatnot throughout the years.
JASON: That's awesome. I think that, I my first text editor was I think just notepad. I think I remember writing table based HTML layouts in just notepad, no syntax highlighting, go chaos all the time.
You always miss something, a closing bracket or whatever. And it doesn't work and you have to go character by character and hope you notice the problem you had. Those were the days, huh?
[ Laughter ]
ELY: Yeah, my very first actual code editor was Home Site. Do you remember Home Site?
JASON: I don't remember Home Site.
ELY: It was a macro media product back in the day. Yeah, it was basically, you know, a text editor, but it supported HTML and stuff out of the box. It was awesome stuff. That's bringing me back into the day.
JASON: It really is. I'm kind of flashing back to my big chunky, like, box monitor and my the tower and under the desk and in my parents' basement hacking on my MySpace page. But we've come a long way since then. And, one of the things I think has been really interesting, kind of as we watch tools evolve over time is we went from this world where, you know, if you want to check if your code is good, you take a copy of the last version of the code, put it into a folder, and then you publish your change, and if something breaks, you drag the old folder on to FTP. No testing strategy, no assurances whatsoever. It was just like, let it rip. Upload it to the FTP and hope for the best.
ELY: Or if you're me, you would RDP directly into the server and make the change on the files right there and see if it would work.
JASON: Oh, my goodness, the real YOLO coding but now, the world of frontend has matured. And we've started to see tooling get built around how to automatically make sure that our code is high quality. And that ranges from LinkedIn tools, we've got things that are prettier to make sure the formatting is good. And we've got type checkers, typescript and then we've got this whole suite of testing and automated testing that is really fascinating and that brings us to where we are today at Cypress. How does Cypress fit into the modern web dev stack?
ELY: As web has grown up over the past decade or two, we used to have just webpages and they were kind of like everything inside of a webpage, all of the buttons HTML and CSS and that stuff, it was a Hodgepodge of HTML. And we had frameworks that encouraged us to componentize those items, to make them reusable and whatnot. So frameworks like React and Vue and Svelt. These small, reusable components. And before, you know, we joked around about making ad hoc changes, but as businesses become more and more reliant on their web presence. It could be fairly costly.
JASON: Extremely, yeah.
ELY: We got these tools to help us test to make sure when we make changes we're not bringing in bugs and whatnot into the production environments and so one of the tools is Cypress and Cypress, you know, hopefully you know what Cypress is, if not, we need to be doing a better job out there. We're pretty ubiquitous in the web ecosystem. But Cypress, our what we're really known for is doing end to end testing. That is where uh yo visit a URL and then through code you drive user interactions and then, you do some things and write some assertions to make sure that the web app is in the state you expect it to be in.
JASON: Yeah, and so, from the perspective of like me as a web developer, what I need to be sure of is that the change that I just made doesn't cause production to go down. Like you said, it's getting much more costly. And it's also way harder to fix. When we're talking about web dev in the '90s where you're uploading an HTML file to an FTP server, you the path to fix it is you fix that HTML server. But over time, as the web got more complex, now we've got varnish cash. We've got PHP running in the background. We've got cash running. We've got a database somewhere. And when you make that mistake, it's not oh, just upload the old version of the HTML, you've got to unwind your cache. You've got to make sure they aren't resetting each other in a way that causes an infinite loop of broken stuff. And you've also got so many dependencies and complexities that unrolling isn't just, you know you don't just get to say, like, OK, roll back. And that's true, you know, you can use tooling to make some of that one click rollback work. Like, you know, I know planet scale's got a one click rollback and Netlify's got a one click rollback, it can be possible. But it would be better if the thing didn't fail in the first place.
ELY: Yeah those are just the things happening in production, usually on development teams, we have processes we have to go through. We have to make a PR into the repo and that PR needs approvals, which means other people have to be around to view it and approve it and get it in. And the CSCD systems kick off. And these tools were built to help us but added a certain amount of overhead into making that deployment.
JASON: Mm hmm.
ELY: They say, if you can catch a bug in development, it's much cheaper to fix while it's in development versus once it gets out to production.
JASON: You know, and you bring up that actually is such a critical thing to sort of internalize. Right? Because it's not just that we have the tooling. Right. Like you can think, I have an automated test. I've done my job. I've done all I can do. But there's also the where the automated test happens. And I think the most expensive thing that I've ever been a part of as the development team is all the tests running only on the production deploy. You're in a local development environment where all of these things aren't actually running and then, you go the development environment, the staging environment and only a few of the tests are running. And then, there's this really restrictive set of tests on production because it takes 45 minutes for those tests to run. Right? And so, then you come back and you get an error. And so you've just, you know, everything was working, everything was working, everything was working. 45 minutes later you get told, this thing broke. And then, you have to dig through some logs, and then, you do the whole song and dance, again. Or you have to wait, you know, 45 minutes to run the tests on your local box, right? And so, what you just said about solving or catching bugs in development is such a critical thing because with tools like Cypress and with modern IDEs and the way that things can be integrated, you can be running these tests as you go. It's like, when I'm in a file and I do something that typescript doesn't agree with, I see something get underlined in red and I hover over it, it tells me what I did wrong, I fix it, and now I don't have to wait for typescript to fail on the production build.
ELY: Mm hmm.
JASON: Cypress is giving us that, right? I'm making assumptions here. Cypress is giving us the ability, the same way that typescript catches our type issues in our code, Cypress can let us know during development whether or not we broke our UI.
ELY: Yeah, and that's one of the things where component testing in Cypress really comes into play. So if you think about the old school testing pyramid, right? Where you have the end to end test at the top and the unit test at the bottom. Typically, you would think lots of unit tests because they're easy to write fast to execute, they need to be reliable. Something in the environment should cause a unit test to start to fail or something like that.
JASON: Yeah, yeah.
ELY: And you have end testing where you write fewer of them, they're typically slower to execute and they typically can have a little bit more of what you would call flakeyness. A server wasn't responding in a certain amount of time or something just happened or whatnot. And so as a developer, if you're using end to end testing currently in your stack, whether if you're writing your end to end or a QA team. Running the full end to end end to end suite is not normally what you would do. It's something that happens afterwards. But with component testing, we're actually bringing some of the power that you get in end to end testing further down the stack to make it more like unit testing and integration testing. And so, component tests share a lot of the same factors that a unit test does. They're quick, they're easy to write, easier to set up. You can actually run them while you're coding and do like, you know, if you do test driven development or something like that. I like to use it as a developer tool that will help me as I work on a particular component to have the tests up and running to be my test basically, my workbench, test bed as I'm kind of working on this particular feature to make sure it's working properly.
JASON: And the way you would describe that, then, you are in the same way that we hot reload our development UI. You've got Cypress running alongside and whenever you save your code, it reruns that test and lets you know if you're passing or failing?
JASON: That's powerful stuff. It, again, brings the experience to an instantaneous feedback loop. I see a problem, I fix the problem, I don't have to break my context or remember what I was working on when that thing broke or, you know, put down this thing to go back to this file and correct that issue. And that, those are like, I don't know from my perspective, one of the most costly things that happens on development teams is that, like, the feedback loop is too long. And so the developers move on to another task. And then, they get interrupted by a failure, and then, they have to, you know, stop one task, move back to the old task, try the fix. And, you know, the flakier something is, the more likely it is that developers end around that process entirely, and that's where you get the it works on my machine thing. Or they just are, you know, they're completely hamstrung by their inability to iterate quickly because there's so much context switching. And that's I bet if you had somebody go do an economic analysis of how much it cost businesses to have slow developer feedback loops, it's got to be right up there with like just spinning up the most powerful AWS machine and letting it run forever. [ Laughter ] Right. It's so expensive to have that many people in this industry where, you know, it's incredibly expensive to hire engineers.
ELY: Mm hmm.
JASON: And to waste their time by having them go this far and then get yanked back to another context and back to the other context. God, that's expensive.
ELY: Like you said, it's all about tightening up the feedback loop. And Cypress component testing, so component testing isn't anything new, really. We've been doing component testing for probably the past 4 or 5 years. Especially in the React ecosystem, you might be familiar with tools like React testing library. That just encourages you to mount up a component and test against it and whatnot. How Cypress differs from that is that we are actually rendering the component and the same the same test environment that end to end will run in. So your component is being rendered into a real browser versus some of the other tools are rendering into like JS DOM and outputting the HTML. It's not actually inside of a web real web browser being interacted with it as an actual user would. And that's kind of one of the advantages that we have. So we'll open up in a real web browser.
JASON: I'm going to interrupt you really quick. I feel like you actually touched on something for somebody who is new, and I see we've got a couple of folks in the chat who are new to web dev. So, you just said that Cypress is opening it up in an actual browser whereas other tools are opening it up in JS DOM. And that is a, I think, the critical difference I've noticed is that when you're testing, right, you want to you want to test as if you had a QA engineer whose entire job was to run through a playbook and open this page, click this thing, add an email. When you click submit, does it go to the right thing? I've worked at IBM, I've written these QA checklists, but humans are human, they cut corners, miss a step, and you'd always kind of run these issues where somebody would fail to do something by following the list wrong or they would say something was fine because they actually didn't do it. And then, you would find out later that it was broken. So Cypress is a robot that has that human that human like application of I'm in a browser, I'm actually clicking, I'm actually typing and doing key strokes. So that it's a real experience of using your website versus a simulated experience that's run in JS DOM, which is great. Like, I love JS DOM. But it is not a browser. It is a kind of a simulation of the browser, which is, you know, it's the same reason I really like when I'm working on a PR, I want to deploy preview that's in the same environment as my production environment to look the a them side by side. I don't go straight from my local host to production. You know, I want to see it running in the environment first. So anyways, that's a little bit of a tangent. But I feel like it's an important thing, especially for somebody hearing about Cypress for the first time.
ELY: You're saying it's kind of like a robot. You're taking the playbook that people run through manually and codifying it. You're making it repeatable, not necessarily human. So you're taking all of those action steps, visiting web page, clicking the button, sending the email. Those are all things you do through code now versus manually like stepping through and doing it.
JASON: Yeah, I think that's great. OK. And so, what we're doing with end to end test is we're booting the whole website and running through critical user flows. And that's, you know, got to do it. It's so important. If your website makes money, you need to make sure that the critical happy pads are working at all times. That's definitely not something that you want to have to wait for every time you save your file.
ELY: Mm hmm.
JASON: You're telling me that component testing is if I can drastically oversimplify this. It is a tiny end to end test for one piece of your UI.
ELY: Yeah, the biggest difference is that in the end to end test, you're visiting a web page. Like an actual URL, whether that's running locally on your server, or staging environment or somewhere, whatnot. You're hitting a URL just like a user would. And a component test is mounting a component. And only rendering that component to not everything else that needs to go along with a particular web page.
JASON: Gotcha, gotcha. OK. That makes sense. So let's talk about what we're going to do today. So, maybe just walk us through the high level, what our little project is we're going to build together.
ELY: We're going to do a little bit of role playing. Jason and myself are developers on a team. And we just got some mocks from our design team. And we're going to work on building a new log in form for our website. We decided we're going to create a new product and the very first thing you do on the new product is create the log in screen, right?
JASON: Mm hmm.
ELY: They gave us HTML and CSS we're going to use and we're going to take that and we're going to drop it into an app and we're going to start writing some component tests along with it.
JASON: All right.
ELY: It's kind of important to point out. We mentioned these frameworks that you can use to build components. And so Cypress right now has support for React Angular Vue and Svelt to do component testing with. And so, today we're going to do a React project and work with that.
JASON: Gotcha, gotcha. Yep. Do a little React. OK. So I'm going to switch us over into programming mode here. This button that I couldn't find there it is. So here, here we go. We are talking today to Ely, so make sure you go over and follow on the old Twitter. And this episode, like every episode is being live captioned. We've got Diane here from White Coat Captioning taking all of that down for us. That is available on the home page of the site learnwithJason.dev. And that's made possible through Netlify, NX, New Relic and and soon to be Pluralsight. Thanks so much to them for making this possible. And we're talking today about if I can get this window, Cypress. So Cypress itself is a very cool free tool, we can just run it in start building stuff with it today. And that is the extent of the knowledge that I have. So you said you wanted to start by bootstrapping a project. So I created a folder, this Cypress Component Testing React.
JASON: I'm going to use Vite. Does that work?
ELY: That sounds great.
JASON: I'm going to use Vite because I love Vite.
ELY: Along with the libraries we support, there's various metaframeworks we support, as well, in React, we support Create React app and next.js and Vite, Vue, Vue CLI, Angular is Angular and Svelt right now is I think whatever the default one is. I'm not as familiar with that ecosystem right now. I'm starting to get into it.
JASON: OK. So now we have oh, we've got a basic site. So I'm going to stop this server. I'm going to open this up. And wait, hold on a second. Close this. We're going to get into it otherwise it ignores my folders. Try that, again. There we go. And so, we're looking at pretty standard app. We've got the app, the app itself has all of this stuff, which I'm going to delete, right?
ELY: Yeah, this is stuff we're mainly going to be working on our component, which we'll probably drop into our application, if E with want to run it through the Vite dev server, we could drop it into the app.
JASON: OK. I'm going to clean some stuff out. We will work on this as if it were a home page is the log in. We'll make that happen. And then, I'm going to delete stuff here, get us all the way down to a nice, clean dev site. And I believe the way this will work. There it is. This is exactly what we want. Vertically centered deal. So as we build our component, it'll sit in the center of the page, which is what I want.
ELY: Actually, for the first step, let's take a look at the mock that our design team sent over to us. And kind of think about how we want to break it apart.
JASON: Yes. OK. I'm downloading that mock. Here it is.
ELY: We got our email pretty basic log in field. A couple input fields, it has a button, validation that's going to go along with the title. Just quickly looking at this, I think we can probably create a button component because every website needs a good button component. Probably create like an input field component that can be reusable for our form. And then, maybe the main log in component, as well.
JASON: You said a button. You said an input. Right? Input?
ELY: Yep. Input.
JASON: And the last one?
ELY: And then a log in form. Components
ELY: Components contain other components, so
JASON: All right. So we can start by just doing export I don't want to use default exports. We're going to do export const, we're going to do button and that's going to return a button
ELY: Mm hmm.
JASON: OK. So that's
ELY: Got to click the button.
JASON: Input, it's going to be export function, input, and that's going to return input type text.
ELY: And there's, I think they sent over the HTML for us to use, too, there might be
JASON: Beautiful. Let me
ELY: Just copy out of there.
JASON: Great. Let's drag this over and look at it. OK. So we've got we've got here's our button. So I'll just take that and put it in here. Reactify that a little bit. And there's our button. And then, we've got our inputs, which let's see these are a label. Here's the label, there's an input, they've got a type class, invalid, error message.
ELY: Let's start with the button and work on it.
ELY: Get the button up and running. Here, we've got the button, let's get Cypress installed on the app and component testing set up.
JASON: OK, NPM install dev?
JASON: And is it just Cypress?
ELY: Just Cypress.
JASON: Do I need anything else?
ELY: No, that should work.
JASON: So Cypress is an NPM dependency and also an app, right?
ELY: Right. Yeah
JASON: That makes it a little bit bigger.
ELY: Architecturally, what happens when you start Cypress for the very first time, if you haven't already done it, it's going to download our binary, and our binary is an electron application that's kind of the launch pad to get inside of Cypress.
ELY: So to launch it, yeah, you've got the latest version of Cypress there. Yeah, if you want to create a script, you can just say tester. Whatever you want to do and the command would be Cypress open.
JASON: So then, if we run NPM run test.
ELY: This is where it's
ELY: There you go. And so this brings you one of our key goals of Cypress is just to make the developer experience get it up and running with it as seamless as possible.
ELY: When you hop into Cypress now, like I said, we were historically end to end testing tool, but now when you start it up, you get to choose your own adventure type path. And we're going to go into content testing. And you see the beta, we launched component testing into beta. Here in the next few weeks, we're going to launch component testing GA. So definitely look forward to that.
ELY: There's just going to be a few minor changes to go along with it. But if you're looking at getting started with it, don't let the beta flag scare you away. So you can
JASON: I can see it picked up. Yeah. One of the things that is so underrated is just auto detection. We've put all of the information in our projects, save me one click, I'm so happy.
ELY: Yeah, and we go through like extreme measures to try to make this as seamless as possible for you. So we automatically detected your UI library, your bungler you're using Vite and web pack right now. This is doing an dependency check. You're going to have them all, but if you were missing one, it's going to let you know which one you're missing and give you the command on how to install it.
JASON: Got it.
ELY: This one is letting you know the files we're going to scaffold for you.
JASON: You're going to do it for me?
ELY: Yeah. This first one right here is probably the one of the most important ones, it's the configuration file. We also detected that this project is typescript, and so, we're creating a typescript configuration file and typescript on all of the other files. And you can see that up there in the configuration where setting your framework to React and your bungler to Vite and why this is important is because when you're actually running component tests through Cypress, we're using your development server.
JASON: Ah, nice.
ELY: We're not firing up our own custom development server, your components are going to be running through the same server you would use if you were developing your website.
JASON: Got it. And again, that's why, like, it's really important that that's the case because the worst feeling in the world is when you realize that the feature isn't broken, your environments are incompatible. And now, instead of building a feature, you're debugging your dev environment versus your staging environment, versus your production environment. And that is, like, it just drains my will to live. When I realized that my boilerplate not the app, but the stuff that runs the app is the thing that's broken, I just it just melted into oh, good, another one of these days. It's the absolute worst. So, using the same environment for everything is such a big deal.
ELY: Absolutely. Next one is component file. This is a file that runs before each and every test that you're going to run. And so this would be the entry point into any type of things that you want to include into your app. We'll get into that in a little bit. Think of this component file as your main app file for your application.
JASON: OK. And then, you've got the commands file. And so, you can extend Cypress's API by creating your own custom commands. Which is really helpful to do common steps going through an application or whatnot. But that is where that happens. And this component index, that HTML is the main index file that we mount your component into. You can kind of think of it if you have a public/index file that your dev server's going to throw up.
ELY: This is that but for your component test. The last file is a fixture. And just an example of how to write a fixture with Cypress, you can mock API requests. And you can these fixtures are the responses to these API requests, if you want to put them in a JSON file versus having to put it in your actual code.
JASON: Gotcha, totally makes sense. And I just saw Jen just rated. Hello, Jen, thank you for coming over. All right. I'm going to continue here. So what we've just done, thus far, is we've set up here's my Cypress config and it looks like the other pieces are
ELY: You got a new Cypress folder at the very top. SKRA JA here it is, got it. Up at the top. And now I have to choose which browser. I'm using Edge, so I'm going to hit edge, is that cool?
ELY: Sounds good. We also have support for, you know, Edge and Chrome, all the chromium browsers and recently got support for Web Kit, as well. That's an experimental feature you have to install some additional dependencies with it. But we're working on getting that all ironed out. OK. And now you're in the test runner. You can see that you don't have any specs in your app right now, so it's showing create new spec. We're not going to do this right now because this experiment isn't that great for React apps. In the future, what's going to happen, and this is what happens for View right now, but you'll be able to hit this and you'll be able to see a list of your components in your app and select the component and it'll scaffold out a test for that. But right now, we don't have that for React. So we're going to manually create a test for it.
JASON: Gotcha. I'm coming in. And how do you do this? Do you put your specs next to your components? Or put them in a spec folder?
ELY: Typically, we say co locate tests with code. And we could say if you want to put them in a specs folder, that's also OK.
JASON: This is the pattern, is that right?
ELY: No the pattern is .cy. The file extension. And there's a reason why we did that. And that's because we know for a lot of existing projects, you're probably going to have a ton of other types of tests that are currently used .spec and .test and stuff like that. We wanted our own extension to be able to find our test for.
JASON: So you don't have to do a bunch of config to figure out which ones are the right ones to look at and so on and so forth.
ELY: Now, we're in here, and we're probably ready to start writing our very first test. And so our testing code is based off of Mocha. You know if you ever use Mocha or Jasmine or something like that, you're used to organizing your tests into describe context and then individual tests using the test methods and whatnot.
JASON: And so, let's see if I remember how to do this. We go describe
ELY: Mm hmm.
JASON: And then, you pass in is it
ELY: It's going to take two parameters. The first one is going to be a string that's a description of what the spec file does. And so here, we're going to be doing the button. And the second parameter is going to be a function. That gets executed.
ELY: There you go. So you can see right now, like hover over describe, again, you're getting red squigglies.
JASON: Cannot find describe.
ELY: So for some reason Vite is having trouble finding the typescript global for these. And let's go and just add. Not the node one but the main one.
JASON: The main one is where?
ELY: Right above it.
JASON: Oh, here. Jeez.
ELY: In that include, add the Cypress folder, as well. I think that should fix.
ELY: that issue.
JASON: And back we go, and it's happy.
ELY: Now you're going to get when you run your tests, which is always awesome.
ELY: So the next thing is when I'm starting off, one of the very first things I like to do is mount the component and make sure it mounts properly.
ELY: Let's write a test. This is going to be the it method, or test works, as well, they're aliases to each ere O. And you can say it mounts. This takes in a function. And now, the command. Earlier I described the difference between end to end testing and component testing, end to end you visit a web page and component. And you give it a URL. But in component testing you're going to use cy.mount and pass in a JSX stream for a button component. .
JASON: So we use the button, which is in there.
ELY: It imported it for us. And yeah, let's give that a view and see what it's doing.
JASON: Cypress and
ELY: And you'll have to focus there and bring your browser back up.
JASON: OK, great. So it looks like it's doing the thing. Do I click into it?
ELY: Yep. We got a button and it's rendered over here on this right hand side, we call this the area under test or AUT. And this is a real browser, so you can actually play around, you can click your components, play around with them, you can use the developer tooling. To inspect it, play around with the CSS, debug, all that kind of fun stuff.
JASON: Yeah, so we can let's see, I can whoops. Yeah, so it's all here. Very cool. Very, very cool. All right. So that feels like the hello, world of testing as you mount the component and it's there. Great, you've done it testing is set up. And, you know, it's a meaningful test in that I have absolutely forgotten to return my component from my return the actual mark up from my components before, and I'm like, why isn't anything on the screen? And so, that part is definitely helpful. So what should we do next? Like, what is worth testing out a button?
JASON: I'm going to do this. I'm going to take this bit and throw it into our app.CSS.
ELY: That sounds good.
JASON: And find out how well that functions.
ELY: And so, here, we've got CSS variables set up and whatnot, a custom font that we're using. These are all things that the site overall depends on and we have the CSS forward button.
ELY: Using React, we have a billion ways to do CSS.
JASON: I tend to love these because then I can just come in over here and I can say we're going to import styles from .module.css. And then class name is going to be styles.button.
JASON: Theoretically speaking this just worked. Did it not? And it did not work because I've got my app.css, how am I including this? Let's take a look. Ah.
ELY: You're on the right track. Remember that one file that I said kind of like mimics our app file on our application and if we're not
ELY: We need to set that up. We're not mounting your app component, we're mounting the button component. It's not getting any of the global CSS right now. And so there's a couple of ways to do this. You can add we won't do it this way, but if you had external CSS you wanted to include, you could add it here to the index file. But the component.ts file. You can import the CSS in that. And that will get it into your so the file directly below this one.
JASON: File directly below this one here.
JASON: And we will import up whoops. Source to CSS.
ELY: Yep, I think that's it.
JASON: That'll do it. Hey!
ELY: There we go.
JASON: Now we've got a button.
ELY: The last thing we're missing to getting our component looking correctly, we're using a custom font, and the font's not that different, but the designers are going to notice, they have laser eyes for those types of things. And so our font, if I recall correctly is a Google font.
ELY: And I think it's a part of those assets that were sent over on
JASON: OK. Let's look over here. Here's the font.
ELY: We can add this to that component index file.
JASON: Got it. OK. I need to add that into
ELY: Up in the Cypress folder.
JASON: The Cypress folder here.
ELY: So this is the index file that we mount your component into. You see online 16, that data/root directly into there.
JASON: OK. And then I'm going to also make sure it shows up in my
JASON: At itself. OK.
ELY: Very good.
JASON: And are we done in this component index?
ELY: For now.
JASON: OK. Close it for now. Don't need the font anymore, we've got it in. Should be good on global CSS. And then, we're looking at here's our app. We've got our button CSS, we've got our button mark up. And over here, we have a font.
ELY: A font, yep. And with the designer eyes, they're going to catch it's the new font now. But, you know, if you wanted to, you could inspect it and go to the computer and see it's rendering the font it's supposed to.
JASON: Mm hmm.
ELY: Now we have our component is looking correctly, let's make sure it's going to behave correctly.
ELY: A few of the things I can think of when you have a button, you want to make sure it's going to support whatever text you put into it.
ELY: Let's write this in a TDD type fashion.
JASON: Good call. Let me go to the test. And then we want it text. And see if I can so we would want to do cy.mount button. And then, we would do like text is custom. And then, I need to capture this.
ELY: Nope, actually, you don't.
JASON: Oh, I don't.
ELY: So we'll get that. And then, so now you're mounting it. Now we can write an assertion to make sure that the text actually contains the custom text. And so, with that there's a few different ways you can write assertions, but the one we'll do here is to use the cy.get method. And then, this is going to take a selector and these selectors are like J Query selectors, you can select by elements, class, IDs, attributes. It's powerful, gives you a lot of ways to do things. But what I would probably do right now and this is not something for end to end testing but for component testing, we're confident our button component is only going to have one button. So we can just select button. Just like that.
ELY: And then, we can chain off of this method with a .should. And then, we can say should, this is a method. And inside of the should you can put in have, I think it's have text.
JASON: Like here? Have text?
ELY: And then, give it and then the second parameter should would be the text we expect it to have.
JASON: Oh, nice, OK. You can see I write a lot of tests in my day. OK, so collapse that down so we can see what's going on. So we've got a component over here and we're trying to pass in some custom text and then verifying that works. The ID is already telling us that we screwed this up. But Cypress will also tell us that we screwed this up. And it is now failing.
ELY: Go ahead and hit the little refresh button on the right. There you go. And we'll watch this run. And so, you can see it spinning and whatnot trying to find that text. This is something that we have in Cypress. It's automatic waiting. That's one of the things you're writing tests against the browser, sometimes tests don't happen fast enough. Most of our assertions will wait up to 4 seconds by default for the thing to become true, before it will try to fail the test. And so, you can see it took a little while. It's waiting but eventually it'll time out.
JASON: Mm hmm. And I double clicked that button.
ELY: It stopped it. We'll try it for a second says it can't find the text. And so, now red/green refactor, we see test red refractor green, whatever the saying is. We see it failing and we can go into the component and make this work.
JASON: Text and that's going to be a string. And nope, that's not how that works. I'm very bad at typescript. OK. And then, we can put in text. OK. So then, if I come back out here, I'm going to focus and there we go.
ELY: Cool. We've got our first real passing test. The second thing that we're probably going to want to do is to make sure that when we click on the button that it actually raises a click event that we can tie into. Through a problem. And so, this test could be like, hey, when we click it, unchange should be called.
JASON: OK. OK. So I'm going to cy.mount. And we'll say button. And then, I'm going to do the same thing to cy.get. Button. And click?
JASON: All right.
ELY: And then, what we're going to want to do here. So your button component is going to take an unchange prop. But we need some way to verify that when the button is clicked the unchange prop is called.
ELY: And there's a library, we call them spies, that you can pass in a spy to this function and then, we can query the spy later on and be like, hey, were you actually called? And how many times were you called? What parameters were you called with? And so on and so on. We'll create, inside of your button, on line 14, we'll pass in an unchange prop.
JASON: I'm getting feedback here that I have broken my button by not passing in the text prompt, which I haven't made optional. We can deal with that in a moment. And is this a cy.spy?
ELY: You'll say cy.spy. And do a dot as. And we're going to give it a name so we can reference it later. And so, just say on change spy. OK. And then I need an on change. And my on change is going to default to be nothing. And do an on change, I'm going to let that optional and it's just a function. All right. What's the
ELY: React does actually provide the typings that we can use if you wanted to inherit them to kind of make our button function as an intrinsic element would.
ELY: So to do that
JASON: Here we go.
ELY: So to do that, let's create an interface that the component, that the props will inherit. Call it like button props or yeah, that's fine, too. And you want to let me try to remember the syntax. So we're going to extend on the button props.
JASON: OK. Little replacement here. Because I forget how there we go. We're going to extend.
ELY: Extend and I think it's .react button HTML tributes, there you go. And I think this one said generic. And the generic is going to be HTML button tribute, I believe. HTML button element. Yeah, that's right. OK, and now our button component is going to take in any attribute that you could pass into a normal button component, as well. And now, you don't need to on change there. Because the on change is provided by React.
JASON: Got it.
ELY: You've got it defaulting to a method, which is fine. Perfect.
JASON: Is it
ELY: On click, yeah, sorry.
JASON: Should I change all of these to on click?
JASON: OK. OK. It wants to be very long. Good, OK, that works.
JASON: Going to focus, on click, triggers on click, that's because we haven't written our test.
ELY: We want to write an assertion to ask the spy if it actually got call clicked. And to get to the spy, we aliased it to the on click spy name. You'll use the get method, again. And then you'll put an ampersand in front of this.
JASON: There it is.
ELY: I'm sorry, the @symbol. Whatever that one is called. The @ symbol is how you signal aliases like we did there. And want to say .should and the assertion will be have been called.
JASON: Have been called. OK. So let's go give it a check. And run them, again. And it works.
ELY: Yeah, and if you click on that. This is our logger, and you can see each step that the test took. And then, on line number 4 there or between 3 and 4, you can see that spy actually got called. That's letting you know that the spy called. And then, the assertion happened correctly.
JASON: Got it. OK. So now we've set this up to mount our component. So we're making sure that we've written a valid React component by mounting it.
ELY: Mm hmm.
JASON: Then, we make sure our custom props are doing what they do. It has custom text. And then we're checking our click handler, which is important for a button. It does what we expect it to do when we click.
ELY: Yep, exactly.
JASON: Great, I feel like those are good things.
ELY: That's probably what we need for our log in component right now. So we're good. Well, the only other thing is, so we made we're taking in all of these attributes now that could be, but we're not spreading them across the button component. So let's go ahead and do that. And easy way to do that is if you take in another prop called props.
JASON: So like props here? Or I guess just
ELY: So dot dot dot props. So anything not there is going to be on the props and we can spread this across. And that way if they add a disabled attribute or custom classes or styles or anything like that, those aren't
JASON: Then it just works like a button.
JASON: One of those things you would have figured out as soon as you needed it, but good to catch it early so no one lets you know, hey, you wrote a broken button.
JASON: We've got ourselves a functional button. Does that mean I could actually leave this out?
ELY: Yeah because that will come that will you still want the class thing there, and I think
JASON: That's good, right?
JASON: Yeah, simpler and now we've got refractor code that continues to run and we don't have to maintain that code anymore. Hooray!
ELY: Sweet. So I think we could probably go to the input field next.
JASON: OK. Here's our input field and that is going to be here's a here's the HTML we got. And this input and this input are not the same, but they're close. So I'm going to start us by just setting this up. And then I've got to fix my classes. I fix my classes, it doesn't like this for reasons I don't understand. Oh, we've got a doesn't like our arg stuff. Attributes must form to valid values. So it needs to announce the message. And is that because of this? Like it needs a roll alert or something?
JASON: I really don't know enough. Oh, it's happy now.
ELY: Yeah, this actually fills up
JASON: Any accessibility experts watching, I don't actually know if that was correct what I did or I just convinced it to stop talking. So, please, speak up if you know.
ELY: And so, that actually brings up a good segue about accessibility and how when you're writing component tests how this can help you with your accessibility of your components. And I am by no means an accessibility expert. But there's some tools that we can use to actually not only test our code but we can actually test accessibility, as well. If you want to if you want to take a segue into that, we can explore that.
JASON: OK. I'm going to just put up a little bit of a time here. We're at about 25 ish minutes. So depending on how long that segue is because I do it's super important, we can either finish the form and then come back around and add that or if you want to add it now, we can.
ELY: Let's finish the form first.
ELY: Just because there's a couple of other concepts to get through that I think are important. OK. So you got over here, you've got your input label, looks like we probably need to add some props for the to customize some things here.
JASON: I'm going to close this test. I'm going to say we need to be able to pull in let's do let's define this as an interface. Input props and that's going to extend input html attributes?
ELY: This will be HTML something, html input probably. Input element. There you go. Cool.
JASON: What we'll need is we need a label that's going to be a string.
ELY: Mm hmm.
JASON: That'll go here. And then, we need the type is going to be included, class name. Will ARIA come in with attributes?
ELY: It would, but I think these are we're going to play around with these attributes later via code.
JASON: OK. So we're going to have the error message, we also need an ID because we're missing some way to identify this as
ELY: ID will come over as part of HTML attributes.
JASON: So we don't need that.
ELY: The only thing we want to do, we've got hard coded password in a couple of places. And so, we can if we get ahold of the props, we can use props.name.
JASON: Props.name. Got it. OK. So let's all right. Let's just pull these in here. I'm going to get the label out. And then we need the error message, and we need the props. OK. So then, I'm going to spread our props. And we will use these. And one of these. And use a props.name. OK. I like that. And take that same little bit of code to replace this one here. And class name error, roll alert. Good. We need the label and we need the error message. That's the component itself, and we need to make sure we set an ID and everything on it. So, let's describe our input, that's going to take a function. And we'll say it mounts and say cy.mount input. Comes in from input. And that's going to need because you shouldn't have an input that doesn't have a label. We'll have a label of test and an error message of busted. So that should be like our baseline here. And if I open this up, did it already do it for us? Let me go back. Input. And there's our hello, world working. So we need some styles. So we have those here. I'm going to grab them out. And we'll make a input.module.css. And that goes, and let's see, everything is pretty clear, class name. So I'm going to come back in here and input styles from input.module.css. And do a styles.enable and we will do P styles.error. That's what we want.
JASON: Happy about that. Our font's loaded. We're in pretty solid shape here.
ELY: Yeah, I think visual wise, it's looking where it is now. Let's work on somen functionality for it. A good first test is make sure that error message only shows up where appropriate. And so our criteria for that we don't want to yell at the user. Hey if it's blank please enter it. We're going to wait until the form is submitted. Before we start yelling at the user. And to do that
JASON: We'll give it a label of test and error message of error just to minimize green real estate here.
ELY: Cool, and then we can let's add another prop called submitted that the parent form will pass in, they'll let it know if the form has been submitted yet or not. And this could be
ELY: So we're using a combination of the ARIA tributes and CSS to display the error message when the field's invalid. So we can actually instead of hard coding true, we can set this up to say if it's submitted and there's no value. Show the error message. So this would be props.value.
ELY: And so we could say only shows error messages when actually invalid. And so in your tests, we can do a selector. So a cy
JASON: Should I oh, you want to take these like one piece at a time here? Also, I'm I think to make typescript happy, I need to do like invalid equals nope here. And then there you go.
JASON: It wants that to be must conform to invalid attribute value expression. But why? Because that is
ELY: It's a boolean, right?
JASON: Oh, it needs to be a string. That's what it is. So if I'm do we need to do one of these? Still doesn't like it. I don't know how to deal with that. So I'm going to ignore it.
ELY: Yeah, I don't think it's going to stop Vite from compiling. That must be an ARIA plugin or something you have. I don't think I've ever seen those messages.
JASON: I don't know what's going on with that. Yeah, where does that come from? It comes from
ELY: Act/web hint?
JASON: Yeah. OK. That's something I'll have to dig into. Figure out why that's doing what it's doing. Because this does seem valid.
ELY: On the right hand side, I think it wants a submitted value now.
JASON: It wants a submitted value. So we'll do submitted equals false, right?
ELY: Should show error messages when actually invalid. And so, that would mean, we have submitted and the value is blank.
ELY: And now, we'll show you a little bit different way to write a selector here. And so, we've seen how we can select things by, you know, stuff that's inside of the html, we can also select items based on their text values. And so, we can write a selector here. And instead of using get, we'll use contains. And then, you can just pass in whatever the error message is going to be. And I think you said is it OK, yeah, just error. There you go.
JASON: For the sake of making this slightly less ambiguous make it an actual like error message.
ELY: And if you wanted to get more granular and say, I wanted to make sure a span or particular div tag has this message, then, you can dive deep into the selectors for that. But for our component here, it's probably good.
JASON: OK. So does not show error messages when and so on this one, we'll do a submitted false, but the value's going to be empty. And is it like a just a
ELY: We need to dive on the right hand side and do a should at the end. .should not be visible.
JASON: OK. Expected it not to be visible. Should not exist? This is going to come back empty, right? There won't be anything on screen?
ELY: In the contains, let's get more specific about the selector. Let's pass in a string of span, that's what the error message is in. Let's see if that fixes it.
ELY: There we go. Little confused why it didn't work at first, but not going to dwell on it.
JASON: All right, so we have it mounts properly. Shows an error message when it's invalid, does not show an error message when it's just empty.
ELY: Mm hmm.
JASON: Theoretically, we want to also check no error invalid. And so, if I set a value of then it should also not fail. No error when valid. Great. And what's good about this is the more I'm doing this, the more I'm just thinking of edge cases. As a developer, we're not going to catch these now, there are a lot of things that cause an error, but now I can come in and say, oh, this make that into a sentence. And this is OK, great, another use case. I'm going to throw this in, cover the edge case, and now I'm sure we're going to catch that next time. Since I'm get this feedback quickly is wonderful. That's pretty excellent.
ELY: Sweet. OK. So we can we probably could write a few more tests for the input field but it's a little bit more of the same.
JASON: Sure. Yeah, let's get to the integrated like, let's build ourselves a log in form here.
ELY: All right.
JASON: I'm going to do the setup here log in form.cy.css. Also going to set up the CSS module, module.css, CSS out. And I'm just going to close this because we don't need it anymore. And then, I'm going to come into my log in form already opened that one. Wow. This one. So I'm going to import my styles from log in form.module.css. And then, I'm going to export function log in form. And that's going to turn one of these. But we're going to build it out of our components. So let's start by doing let's see, the legend can stay. This is going to be an input. And it's going to have a label of log in and it's going to have a type text. And probably need to wrap that part up. And then a class name of input.
ELY: I think the class names were already in the component.
JASON: That's right. You're right, you're right. So we need a name of log in. So that it catches user name. And then we need an ID do we need an ID of this?
JASON: Name will catch it. And then, we have the error message we need. And then, if I close this up, is it happy?
ELY: Probably wants submitted, as well. And we don't have
JASON: Well, I'm just going to we'll fix that in a second. I'm going to start here. That one's good. And then, we need one more. And this one is going to be password. It's going to be a type of password. Going to give it a name of password. And we're going to give it an error message of password is required. OK. Then we can drop this out. And then, our button is going to have text of log in, and probably need more stuff, but we can stop it there for now. We need these messages. Oops. Back off. Back off. There we go. OK. And we need to fix few more classes. What else you got? That's it, right?
ELY: Yep, looks good to me.
JASON: OK, let's go in here and we're going to describe log in form. We're going to mounts. And this is going to be a cy.mount of our log in form. Comes in and we don't have any props there set yet. I need to set up our styles. That was the other piece that needed. So styles.form. Styles I'm going to copy/paste this so I can go faster. Need a legend. No classes, no classes. Here's our error. And here is our success. So this should work once I save it. Here, I'm going to focus, I'm going to bounce back, go to my log in form and look at it go! In one try we got the fully designed log in form with the components in place and this is all functional. Like that's honestly, that's pretty excellent that, you know, we did our tests on these components themselves so when we actually went to use them, it did exactly what we wanted because we put the time in to make the components pass our tests.
ELY: Absolutely. How much time do we got?
JASON: We have about ten minutes.
ELY: OK. So there's the log in form itself probably holds most of our actual logic. Especially around making the fields be required and whatnot. In interest of time, let's work on submitting the form to actually, you know, test validation and stuff like that. And so, what we'll want to do here is in let's write a test and watch it fail. If I pass in invalid credentials, something like that. And here, here our backend team is still working on our off end point. But we know what the off endpoint is going to look like. And this is an advantage we get when we use a testing tool. We can actually start working and just mock the API requests and work with it and as soon as we drop it into the application, it should work as long as the contracts are all correct. We need to type in a username and password, write selectors to get inputs.
JASON: And I want I guess we can just do
ELY: That should work.
JASON: That's one way to do it. And then, we want to
ELY: And so we got a type method. And this will all say and put like bad user, something like that.
JASON: So we've got ourselves a hacker. And then duplicate this and get password. And as everyone knows secure password is hunter 2. And then what?
ELY: Now, we want to click the log in button.
JASON: Do we want to cy get button.
ELY: We need to go through and we want to verify that welcome message is displayed now. That will say welcome or no, this is the negative case. We're going to say username password.
JASON: OK, so bad username or password should be visible. OK. Oops, a little bit wider to see what's going on. So we're going to type, we're going to click the submit button, we're going to check that we got a bad username and password. This might actually no, it's mad. Targeted a disabled element. Interesting.
ELY: Oh oh, because the submitted is hard coded to false. So it's the signaling element. I think. Oh, wait, you defaulted to false. Why is that disabled?
JASON: Huh, why would that be disabled?
ELY: Submitted false oh, we need to keep track of the well, I don't know why that would be. One thing we do need to do is keep track of the values through our couple state variables.
JASON: We'll do username, username and that's going to be state, and we'll start it as empty. Duplicate that password, set password. And then, down here, we will set the value to username and we'll set the value to password. And unchange, right? I want to set the event, right? And then, I want to set password to event.target.value?
JASON: Paste that, again, up here. And set the username this time. All right. Now we have controlled elements. And presumably
ELY: Why is it oh. So one thing that it's doing is right now, we're not overriding the form submit. And so back to the server and refreshing itself. So, yeah, we could write a submit handler and then prevent default on it.
ELY: In the code, yeah.
JASON: So we want to function on submit. Will be event and we're going to event.prevent default. And that should get it started in that. Crap. And that needs to be an HTML type int.
ELY: You can use react.form event I think alias to what you're typing out here.
JASON: That's easier than I've been doing. And then we do an on submit is on submit.
ELY: There we go. Now, let's see if
JASON: Try that one more time.
ELY: There we go.
JASON: So we were just submitting the whole form and that was causing chaos.
ELY: Yeah, it was causing a post back to the server and reloading in front of the tests, again.
JASON: Gotcha. So now, it is actually doing what we want. But it's not doing what we want because we don't have any logic here to control these. So
ELY: So we we could I just sent you a little snippet of the in our chat of the fetch requests that we'll be making to the server.
JASON: Do I do that in this one or this one?
ELY: On the in the component itself.
JASON: OK. So log in, need to get an error message. We're going to get one of those. And this runs username and password, that is going to be called here, right?
ELY: Yep. And down below the error message display, we can make that only display when error message has a value.
JASON: OK. Error message in there. OK.
ELY: And we probably want to set error message to a blank screen every time we submit the form.
JASON: Does this need to so if username and password await fetch, if the rest status, otherwise send error message to
ELY: Just, yeah.
ELY: Blank stream will work because that won't false in your test flow.
JASON: All right. Expect it to be visible. Yeah, that is now failing because it's not checking the thing. But we need to log this now.
ELY: If you go back into the test runner really quick. You'll probably see down OK, between line 7 and 8, trying to make a fetch request to the off endpoints coming back with a 404. So we can use the cy.intercept method to intercept this API request and we could either spy on it or we can give it a value to return back and so, that's what we'll do. In our code, you can see, hey, if it's a status code 401, that means a bad authentication.
JASON: OK. So I'm going to just mimic a response here and the response I'm going to send is status code
ELY: 401. The only thing you're missing, the first parameter is the http method we're going to take. This is the going to be a post request. There you go. And I think that should be good.
JASON: Method URL response. I see, yep. OK. So that should do it, right?
ELY: I think so. Give it a shot.
JASON: Reload. Still got a 404, which means I did something wrong. Let's look, again. Cy.intercept. Do I need to run this ahead of anything? Ahead of mounting?
ELY: Cy.intercept post. Let me look, auth, status code oh, you don't need to return on this this is just the third parameter's not a function. It's the object you want to return. So you can take
JASON: Oh, oh. Made this too complicated. OK.
ELY: I'm a little surprised the function didn't work. It probably should work.
JASON: There it goes.
ELY: There we go, sweet. And we could do the same thing for the happy path here, but it's kind of repeat of the process. But this is just
JASON: A copy/paste. All right. So that puts us actually at time. So, this I mean this is cool stuff. And it's really you know, you can see how quickly this starts to build really resilient code. I can definitely see a lot of value in this. And that instant feedback as opposed to having to stop coding, run the tests, then, you know, respond. It just being able to see it. If I was using two monitors here, I would have it just running off to the side. But since we only have one streams, I have them stacked. But I would run them side by side to see what's happening and have my extra input as I go to make sure I'm building resilient code. And that's very, very cool. So we're going to call this one a win. And I'm going to take us over to, let's see, we've got your Twitter, which everybody should definitely be going to follow. We've got the Cypress home page, you should go check that out. Where else should somebody go if they want more informing? Information?
ELY: Cypress Center Docs is the best place to get started as well as component testing. We've got the component testing section down there and there are getting started guides and whatnot. And then, another resource up in the menu, you'll see the learn, this will bring you to the learn.Cypress.io. This goes into testing practices in general, the best practices to go through, what you should think about as you're writing your tests and whatnot. A little bit more completely free, but it's more premium educational content, not just kind of dry documentation content.
JASON: Gotcha. Cool. And I'm going to do one more shout out to our captioner, we've had Diane with us today. Thank you so much for being here. That's from White Coat Captioning. And that's made possible through the support of our sponsors Netlify, NX, New Relic and Pluralsight kicking in to make this more accessible for more people. Check out the schedule, we've got a lot of good stuff coming up. It's more than I have time to talk about. But you can add it on Google Calendar, you can follow on Twtich here. You can subscribe on YouTube so you so new episodes. All of those things help to get the show out to more people, which I very much appreciate. Ely thank you for spending time with us today.
ELY: Awesome. Thank you for having me, excited to be on the show.
JASON: All right, y'all, we're going to find someone to raid and we'll see you next time.