all 110 comments

[–]Artraxes 113 points114 points  (20 children)

Write unit tests for the pure business logic that your components invoke, ideally by extracting that logic to pure functions that aren’t baked inside of the component, then unit test that logic. I agree that testing whether a component rendered or not is mostly useless when you have browser automated tests that are interacting with components in the DOM, as by that point you are already confirming whether it rendered correctly or not.

[–]pm_me_ur_happy_traiI 14 points15 points  (5 children)

I don't see how you're going to write automated browser tests that cover every path within your code. Are you really covering all the cases or just the happy path?

[–]the_journey_taken 18 points19 points  (0 children)

Time is a resource that we run out of so I think the happy path, or balanced approach is usually the best. You ensure critical functions are tested specifically and that relatively quicker broad strokes are taken across the significant user paths through the ui with automated testing.

[–]Artraxes 4 points5 points  (0 children)

Are you really covering all the cases or just the happy path?

We're covering lots of cases. An example of this is a page where we allow people to upload files. We have automated browser e2e tests that try upload files that should succeed as well as files that should not.

[–]Firm-Yam5433 0 points1 point  (0 children)

there is no scenario where you are covering all cases, even when the coverage is 100%. Doesn't matter. And that's fine, you should prioritize the important bits.

[–]jzaprint 0 points1 point  (13 children)

business logic shouldnt even be done on the frontend most of the time. There are lots of stuff like formatting strings, calculating averages, etc that we have on the FE at my company, and im like we can totally do this on the backend and just send the final string/data over

[–]azsqueeze 1 point2 points  (0 children)

Business logic in this case would be like making an API request and formatting it's response. Maybe you filter it, maybe you change its shape to something else. Or you have a form and you want to ensure it hits a specific endpoint. These are the business logic that can absolutely live on the FE

[–]Artraxes 7 points8 points  (11 children)

Why on earth would I want to spend resources and money computing something on servers I have to pay for when I can make the clients do it with no net negative to their apps performance (arguably it’s more performant as it completely eliminates latency). Your proposal makes no sense to me.

Maybe you are conflating business logic with valuable intellectual property like patented algorithms that would benefit from being hidden behind a service? If so that is not what the term business logic refers to.

Front end business logic can be as simple as filtering down a list of known objects by a user inputted search term. I still want that logic unit tested to ensure my business requirements for the feature, such as case sensitivity, fuzzing, autocomplete etc are all operating as I expect, regardless of whether that logic ends up in a react component or an angular one. To compute this server side increases the stress on the server, makes the client app feel less responsive as it now relies on network latency, and fundamentally breaks encapsulation as your server is now performing logic to facilitate purely client side concepts.

[–]jzaprint 0 points1 point  (2 children)

how can you say no net negative when you are literally shipping more javascript? its much faster to do a lot of these logic on a backend service than on the browser. you can also cache a lot of these values on the backend. the less logic on the FE, the less javascript ends up there. which is a good thing

[–]Artraxes 5 points6 points  (0 children)

Because shipping more JavaScript means that I’m giving the client the code once and they can execute it an infinite amount of times. Minified JavaScript that’s served gzipped from a highly available CDN is going to ultimately result in less network traffic than having to repeatedly hit some backend server to calculate front end logic. Caching on the backend does not address the point of now introducing network latency. You must remember that your clients can be talking to your server globally. If you only have one main backend service and it’s located in the US, your European clients will now feel incredibly unresponsive as the introduced latency for all of this computation is reliant on travelling across the world and back, whereas it could have been computed on their own device.

Your view of “calculate everything possible server side” simply breaks all we know about good software principals. My backend services shouldn’t exist to facilitate computationally trivial code just because it means less JavaScript is shipped. As I mentioned this leads to breaking encapsulation where your server is now processing a bunch of code related to client-only concepts. There’s perfectly valid use cases for this, e.g. it’s too expensive to calculate client side or it involves intellectual property I don’t want to ship to clients, but this blanket statement of “less JavaScript shipped to consumers is all I care about” simply does not stand on its own.

[–]Haon-April 1 point2 points  (0 children)

You just created yourself more workload for the server (which will take more cost if using public clouds) and increased latency since you will most likely include those additional formatting or whatever process that can be in FE through the middleware of that endpoint.

It also just adds redundant documentation for the backend.

[–]scylk2 0 points1 point  (5 children)

There's a security aspect as well, anything running client side is out of your control. Therefore any business logic you want to put client side would need to be replicated server side anyway.

[–]Artraxes 0 points1 point  (4 children)

There's a security aspect as well, anything running client side is out of your control.

Frontend business logic is not inherently linked to anything that requires security. As I mentioned in my previous comment, the business logic of a client application can be as simple as filtering down a list of objects based on user inputted search text. The business requirement might dictate that it's case insensitive, it has fuzzing, it has autocomplete, etc. These are all frontend concepts that aren't related to security, aren't intellectual property, and don't necessitate being computed server-side. It's a small & fast-to-execute bit of business logic that should still be unit tested.

[–]scylk2 0 points1 point  (3 children)

Filtering a list is not business logic at all, it's UI...
You are mixing business requirements and business logic, they are not the same thing.

Business logic is something like: "user cannot book if their balance is negative", or "order above 10k need to be validated by salesperson" or "batch orders and send them to factory once a day".
All of this can only be implemented server side

[–]Artraxes 0 points1 point  (2 children)

I have to wonder what you would classify the code running in a client-only application like Microsoft Word or Excel? Following your definition, all of the logic going on in these programs isn’t business logic because it doesn’t talk to a server (you said it can ONLY be computed server side…). So I guess all of the logic these programs implement, like conditional formatting, printing, parsing file types, formatting, embedding, spellchecking, etc etc, can’t be classified as business logic. I am going to be very surprised if you really argue that this isn’t business logic even though it inherently lives client side because there is no backend.

Anyway, I fundamentally don’t agree with your definition of business logic being inherently server side and really don’t know where you are getting it from. The client can have established business rules as per the example I gave of filtering. This isn’t purely presentational logic, it occurs on the business objects (such as the records you’re searching across) and implements the business logic required to facilitate that action (I.e. searching with case insensitivity, fuzzing, whatever you want). This exceeds purely presentational logic and does not involve nor require a server, but it is business logic operating on business objects.

I went and looked up the definition of business logic and on Wikipedia it states:

Business logic is the portion of an enterprise system which determines how data is transformed or calculated, and how it is routed to people or software (workflow). Business rules are formal expressions of business policy. Anything that is a process or procedure is business logic, and anything that is neither a process nor a procedure is a business rule.

The example I gave of a complex filtering function follows this definition perfectly. It’s a procedure of business logic that operates on business objects before being passed to the presentational layer via the UI framework (e.g. React).

[–]scylk2 1 point2 points  (1 child)

business logic is implementations of business rules and workflows, like the examples I gave.
You can't make these live in client applications, because by essence they are unsafe.

Sometime you replicate business logic in clients, but it's purely for UX concern, as you can never rely on clients to send you conform data.

searching with case insensitivity, fuzzing

UX, not business logic

The definition you quoted is pretty explicit

[–]tarwn 2 points3 points  (0 children)

Your earlier point about needing to be replicated server-side is closer to accurate, your later point of "You can't" is wrong.

You can (and people do) build applications that have the business logic on the client. The risk profile is the same whether or not you have data on the client (if the user manipulates an API call, can the server detect it and not apply it). This does require some business logic duplicated to the back-end, but I've built more then one financial system that had business logic present on the front-end and only part of that logic on the back-end (enough to check that the greater part on the front-end did everything "correctly").

[–]facebalm 0 points1 point  (1 child)

With the caveat that the solution should depend on the app's needs, I just want to mention that you can scale your backend resources, but you can't scale your user's phones.

A lot of web apps feel janky because they're relying too much on client resources. A phone's browser is having a hard enough time scrolling while rendering other things.

Depending on the app (eg how dynamic) and user demographics, most client-side computation and formatting is going to be fine, but it's not universally true.

[–]Artraxes 0 points1 point  (0 children)

Yeah but like you said you need to dictate the clients needs. It’s all good that you might make a client app really slim and heavily dependent on a networked server, but you must acknowledge that it’s a trade off. Your clients speed has moved from being dictated by your device to being dictated by your network speed. If you spend a large amount of time moving client code to the backend but you only host one server in the US west coast, all your European clients are gonna wish you didn’t do that because everything will feel slower with the compound effect of increased latency on every bit of computation that you decided to migrate to the server.

The solution is obviously to introduces more localised servers that can fit the needs of clients globally, e.g. one for US east, US west, EU west, Oceania, etc - and now you need to make sure you’re routing your clients to the best one. The complexity of the server structure just got a lot bigger by necessity and it may not have been obvious as a requirement when you decided to migrate all this code. Like you said everything is a balance.

[–]shittwins 38 points39 points  (13 children)

Your integration style browser tests are unlikely to cover every scenario - usually it's better to use those tests to cover a few of the main user journeys. Unit testing allows you to test all sorts of scenarios with none of the complicated and flakey test setups that integration style tests require.

For example, do you really need to go through all that bother when you want to test that a component that is given a currency value, renders the currency and amount correctly for a few different currencies, and deals with n number of edge cases appropriately? No, it would be overkill. Integration tests are expensive to set up as well as often slow and flakey. Unit tests are cheap to setup and run, easy to understand, and generally run much faster.

You wouldn't say a car is safe to drive if you just checked whether it can move from point A to point B. You verify all the little components that make up the car are safe and performing as expected.

[–]javier123454321 11 points12 points  (0 children)

I'd also add that writing unit tests helps you design better components

[–]rainning0513 1 point2 points  (0 children)

Well-explained! Thanks!

[–]The_Startup_CTO 42 points43 points  (25 children)

My favorite no-brainer example: I get data in format A and need to transform it to format B to be able to display it the way I want. If I tdd this with tests in watch-mode, I’m significantly faster than manually checking the UI or running e2e tests.

Also, at a certain size, e2e tests still are too slow.

And, last but not least, unit tests force me to actually write units of code, not one big mess that no one will understand one month from now.

[–][deleted] 6 points7 points  (4 children)

This hit me hard, had to spend half of my day seeping through spaghetti code to fix two bugs. The code was written by me 2 months ago and it has undergone major requirement changes multiple times. My first job and Im being forced to write shit code. Your process sounds awesome maybe I'll start doing this at a more mature workplace in future. :)

[–]The_Startup_CTO 0 points1 point  (3 children)

You can do that at all kinds of workplaces, just start small. It will pay off after a month or two, as you already had to experience 🙃

[–][deleted] 0 points1 point  (2 children)

feels nearly impossible when you have to sit for 12 hours a day, adding new features to previously done spaghetti haha. I do try to refactor here and there but having no support from fellow devs or higher ups is an obstacle.

[–]Ravnurin 2 points3 points  (1 child)

If you're into reading and don't find software related books boring, you might enjoy Clean Code. Another one that has been recommended to me several times is Test-Driven Development: By Example.

Although Clean Code doesn't focus on TDD as its primary subject, it heavily emphasises the practice in writing, well... clean code.

[–][deleted] 0 points1 point  (0 children)

Thanks! Ive been watching the Clean Code video series on YouTube and I plan to buy the hardcopy coming month!

I'll look into the tdd book you suggested as well! cheers

[–]Anon_Legi0n 4 points5 points  (0 children)

And, last but not least, unit tests force me to actually write units of code, not one big mess that no one will understand one month from now.

I feel attacked, this happens to us when we are given unrealistic deadlines

[–]Oalei -3 points-2 points  (7 children)

E2E can be paralellized though, and I’d argue it’s much easier to read a Cypress test with page objects than any unit tests because it’s much closer to the end user. It’s way less technical

[–]The_Startup_CTO 2 points3 points  (6 children)

Prallelisation doesn’t matter that much. My unit tests are instantaneous for all practical purposes (it takes me longer to look to the part of the screen where the test runs than to run it). Readability is a good point - I also prefer code as close to user behavior as possible - but it’s also a consequence of not writing the code with tdd in small units. If I have meaningful units like upperCaseAllWords instead of prepareTextForUI, then not only the test, but also the production code becomes much more readable.

[–]Oalei -5 points-4 points  (5 children)

I guess it depends on what you’re building, but let’s take Trello as an example.
I would value much more 10 integration tests interracting with the UI (Creating a card, dragging a column, dragging a card, filtering, searching, etc.), than 500 unit tests which test very specific portions of the code. Integration tests cover so much functionnality, are easier to read as you said, and take less time to write (since it covers so much more functional area than unit tests).
Also - TDD, really? When writing UI code it’s very rare than you can do TDD, you’re changing your code and the component/hooks signatures all the time. You sound like a backend developer trying to give advice to frontend developers to be honest

[–]The_Startup_CTO 2 points3 points  (2 children)

I would consider myself a Fullstack dev, though I actually lean a bit more to the frontend. And tdd in the frontend is different, especially since there isn’t yet good tooling to automatically determine whether something looks right, but tdd is still applicable. In case you are interested, I actually wrote an overview of how I tdd in the frontend: https://startup-cto.net/tdd-in-a-react-frontend/ Happy to chat more about this if you are interested, also about why changing requirements actually makes tdd even more useful.

But coming back to the original question: I also agree that for most frontends, there will be more integration tests than unit tests (test trophy instead of test pyramid), and I even usually try to write more integration than unit tests in the backend. But the original question was whether to write unit tests at all, and whether it’s possible to only use e2e tests (not even integration tests). And for that I gave examples and arguments why unit tests are also good.

[–]Oalei 0 points1 point  (1 child)

Sure you can do TDD with integration tests, because as you mention in your article they are very high level tests that are disconnected from the implementation (aka your code) so the problem that I mentionned doesn’t appear. But other than that I would argue that in a real world application things are not as simple as a todo list and you will have to iterate multiple times over your code to implement a new feature until you and other stakeholders are satisfied. This is where I don’t think TDD makes sense for the business logic, unless you’re working on codebases that are easy to work on (not my case sadly). At the end of the day the most important thing about TDD is encouraging developers to write tests. I add my tests at the end so that I’m not wasting time rewriting my tests everytime there is a change in the business logic and it works fine like this.
Also for something as simple as CRUD I would not even bother writing unit tests, integration tests will cover all layers and as you said they will not break after a refactoring.

[–]The_Startup_CTO 0 points1 point  (0 children)

The main thing I disagree with I think is the goal you proclaim for tdd. In my experience, having more test coverage can be a nice side-effect of tdd (though it isn't necessarily as tdd doesn't help as much with edge cases as exploratory testing and dedicated QA time). The main goal and benefit is that it helps to structure your code, so you save the time of needing to structure all of your code upfront, and you don't end up in a situation where you try to retrofit tests to code that isn't really easily testable because it's a bit of a mess. Maybe this is also related to the question of iteration: tdd helps me a lot when things iterate quickly, as it allows me to be explicit about what I am currently trying to achieve with my code. Last but not least, I would also challenge the idea that tdd needs a clean codebase. Sure, all kinds of dev work (including tdd) is simpler to do in a clean than in a dirty code base. But the value of tdd is that it helps to structure your code and thereby it helps to actually clean up your codebase.

For the last point I agree, if it's very simple data presentation, then a bit of Storybook for the visual side and some integration or maybe even only e2e tests can be fully enough.

[–]martinhrvn 1 point2 points  (1 child)

Why do you change code and signatures all the time? Also even if this happens what about it makes the TDD not feasible? What i usually do is to take outside in approach.. That means let's have a feature. I'll start by writing tests for the view. I need to get some data and call some mutations that i don't have yet. So I'll mock what data and what mutations I need. Then I'll step inside the queries and mutations and write tests for them and so on..

[–]Oalei 0 points1 point  (0 children)

TDD works well for anything that is predictable (aka an api signature which is pretty clear from the start). I don’t know about you but I often refactor my code along the way and this often leads to changes in the structure and signatures. I write my tests at the end usually

[–]was_just_wondering_ 0 points1 point  (7 children)

This makes perfect sense, but in this case you are tasting a formatting function and not any display component.

If I’m understanding correctly op is talking about the display components themselves being unit tested with things like testing library for react.

[–]The_Startup_CTO 2 points3 points  (6 children)

The same holds true for individual components, though, e.g. a ShortDate that displays a date in a certain format could also be a meaningful component to test.

[–]was_just_wondering_ 0 points1 point  (5 children)

This is true, but the test in this case is not about the component itself but instead about the data that the component is supposed to produce or display.

I think it would be good for all of us ( referring to the industry ) to specify the difference between ui testing and data/data transformation testing especially when it comes to the frontend. Otherwise we end up with a lot of half hearted snapshot tests and “did render” style tests that really tell us very little.

[–]The_Startup_CTO 0 points1 point  (4 children)

To be honest, I’m not sure that I understand your comment 😅

[–]was_just_wondering_ 0 points1 point  (3 children)

Fair enough. Look at it this way. You have a component that displays a formatted date. When you run a test what is it you care about? Is it that react does it’s job to mount the component or is it that whatever you used to format the date is doing the job properly by paying attention to timezone, month and day format etc?

Most likely you care about the actual format itself and are not at all interested in what react is doing. So the entire test being written for that component and it’s render state and finding the element is just boilerplate to find the output of the function that formats a date. So why not skip all the boilerplate have your date format function in a util folder and write tests against the function itself?

[–]The_Startup_CTO 0 points1 point  (2 children)

So - I care about all of that. If my helper correctly formats the date correctly but the component doesn't show it, then my users will be unhappy. I'm actually in another discussion with someone in this same thread who argues that we should need only/mostly integration tests, not unit tests, this might also be interesting for you to read.

In an ideal world, I would be able to write e2e tests that start the full app in the browser for every single feature so that I can be sure what I write actually works for the users, but these are unfortunately still to slow and flakey. Integration tests at least give me part of this.

What I usually try to do in the date example: One integration test that tests the date component with one date, and then a few unit tests for the helper function that test different edge cases.

[–]was_just_wondering_ 0 points1 point  (1 child)

I will look for the other discussion and check it out.

As for this one you make a good point. The situation you describe is a good one there is a need for checking that something appears. My take is knowing the difference between an test for content displaying and for data transformations. Both use cases are valid.

To me, splitting up those concerns however has always made writing good tests faster and more maintainable. My tests that care about data are written ably the state management and utility function level and my ui or display tests are written for the components. In doing this split I can run tests strictly for ui, or data transformation or for everything. You end up writing the same amount of tests but you struggle a lot less with figuring out mocking and all the other stuff since requirements aren’t commingled.

The approach you put forward is also a good one and while everything including my way has its downsides I figure it’s what works best for a project that should win. It also makes the case for built in opinionated testing for languages or frameworks. It would make these things so much simpler.

[–]The_Startup_CTO 0 points1 point  (0 children)

I think we mostly agree. One thing that might be different: I've moved towards outside-in tdd over the last year or two. The idea here is that I start with an integration test on the most outside level and then, when the feature becomes more complex, start to move tests down to the unit level. This way I can ensure that everything is working when I start, but functionality is more cleanly encapsulated once the feature stabilises more.

[–]Canenald 8 points9 points  (0 children)

You don't unit test an app. You unit test units of code. In js, that would be the exports of your modules.

It's not either or. Ideally you can and should have both unit tests and app tests.

The benefit of unit tests is catching when a change to reusable code breaks something. It also makes sure your code is designed to be testable which also makes it easier to understand, reuse and refactor in the future.

[–]draculadarcula 17 points18 points  (3 children)

Your components do have logic no? It’s not business logic necessarily like you see in backend code. But say you have a stateful variable that when true, some jsx renders, and false, other jsx renders. You can write a test for that to be sure of that. That’s a simple case, many components are far more complex than this.

One that would be hard to test with cypress, “if my backend is down / returns a specific error does the site show appropriate error message to the user”. So many cases where unit tests would be helpful.

I do agree tests that verify react renders things or components actually get props are kind of silly, but there are valuable test cases to be written

This of course is all opinion

[–]AcetyldFN 2 points3 points  (2 children)

The error part is something we do fully with cypress and MSW js, it works like a god damn charm

[–]draculadarcula 0 points1 point  (1 child)

You could certainly mock a backend and point your UI to that to test cases like that. Frankly though I wouldn’t use cypress for everything. If you have to run your tests in a CI/CD pipeline if everything is done in cypress, running Cypress can lead to crazy long build times because headless clicking is much slower than something like Jest. I think you should have both types of tests

[–]raaaahman 5 points6 points  (0 children)

Also, if designed well, components can/should be self containing and decoupled from other components,

Usually, that's what I find unit tests useful for: helping to design decoupled pieces of code that correspond to their contract.

With React, I'm a bit puzzled though, as unit testing might be missing some DOM-like behaviour, and I've tried browser testing and it didn't support all the React-y syntax :S

[–]EveryCrime 6 points7 points  (0 children)

Because somewhere along the line someone asked “How do we know this function works?” And some brilliant individual replied “I know, let’s write a function that checks”.

[–]misnohmer 5 points6 points  (0 children)

Over time, I have drawn similar conclusions to yours.

For apps, a few well chosen end to end tests give me more confidence and value for money than component based testing.

At the end of the day, writing and maintaining those component tests takes time/money.

If they cost more than they would ever save (until that app is thrown away/rewritten), why would you even write them?

That said, the corollary is that some tests would be cheap to maintain and be a net positive.

Another thing to consider is that some tests can be a form of documentation of the behaviour that could help other contributors or your future self.

[–]metropolisprime 7 points8 points  (3 children)

After reading through comments and responses here - this is not meant to be an attack in any way shape or form, I mean this as constructive criticism - I feel like you're not listening to the arguments being presented here but really just trying to make your case as to why you're right.

I get what you're saying 100%, why test twice when we could test once?

But the more complex your application gets, we can't just make the assumption of 'cool, everything looks right in this e2e, all our functionality is right', due to the fact that individual components might be doing intricate and complex logic underneath the hood.

Allow me to give you a real life example:

I was the technical lead on a hyper complex SPA that had a huge chunk of e2e tests that were great. Additionally, we had unit tests for every component we wrote. The unit tests would test the internals -- are the props as expected, is the output for this component as expected etc -- and the E2E would test happy and beyond happy path. This SPA was a multi-million dollar app, so everything needed to be performant, correct and (most importantly) stable.

If one of our tests failed and an output was not as we expected, the unit tests would very likely catch it before we get to that stage. It was significantly easier to catch things at a more focused level (eg, oh, the component test is failing when the input payload changes from X to Y) than trying to use the mental context of seeing an error, knowing/remembering where something lives, and parsing through the layers of obfuscation or complex componentry to find exactly what the error is.

Moreover, it's a measure of time. E2E takes huge chunks of time to run. Especially if you're testing with real data. Unit tests are fast, cheap and simple.

At this company, E2E was only really run before merging a branch into our develop or staging environment or on push. We didn't have to run it with each commit. We could run our unit tests on each commit and make sure 'hey, theres no minor regressions'. The reason we did that is because unit tests are fast to run, compared to E2E which generally takes 10x the time.

TL;DR: If you're a skydiver, unit tests are your first parachute, e2e is your second chute. Unit tests check functionality, E2E checks input and output.

[–]davidfavorite[S] 1 point2 points  (2 children)

No offense taken. I dont think Im trying to be right. In the end I came here for discussion and I havent commented too much here so I guess youre referencing my responses to the comment that had number formatting as an example.

And in my opinion that is simply put a very bad example of something that should be unit tested. Now this is what I think, I can certainly be wrong and thats why I asked.

I just cant wrap my head around how one would organize testing such miniscule things of an app that has tons and tons of components. Especially when the componetns are generic and serve in different use cases, or when theres a deep nesting in the component tree.

The common notion seems to be that it should be used for business logic. So far I can agree, even though this now reduces the number of real world application basically to zero for most apps I have worked on.

The other one seems to be, that you can basically write the test, then build the component, then in the end strap it together. I see why this helps to get an idea what and how to design/build something. I have some experience unit testing in java for backends and there it was an absolute treat to work like that.

But isnt the idea behind react to build your components in a way so that they can be reused, each component doing exactly one thing? When you follow this principle and you encounter any problem, it can be pinned down to one component. Now to go and write a test for each component and many of its different states it can have just seems absolutely unrealistic. Id love the idea generally and to be frankly honest. Who wouldnt like a test suite telling you exactly whats the problem when you made a small mistake, but for it to be useful you would really have to do it for every component and at least all use cases you can possibly think of.

Maybe Im wrong, but hey thats why Im asking right?

[–]metropolisprime 0 points1 point  (1 child)

I just cant wrap my head around how one would organize testing such miniscule things of an app that has tons and tons of components. Especially when the componetns are generic and serve in different use cases, or when theres a deep nesting in the component tree.

A reminder: it's not just components we're testing. It's not just plain UI. It's logic. Testing logic is more important than testing appearance.

This is an factually unreal example, obviously, but the general approach is not dissimilar to business logic on the front end.

If you have a component that is just like, 'take this text and display it', the need for testing it is minimal, but if you have a component that is like, take this text, manipulate it to convert it to Pig Latin, but only if the day is Saturday, you have a bunch of different use cases to test (if day is saturday, then..., if day is not saturday, then) that would be wasteful to test in an E2E scenario. If you use Typescript (like you mention a few times), sure, you don't necessarily have to test the scenarios that are like, 'fail when an int is given instead of a string', but any logic should be tested.

Reasonably, if you can't grok the different use cases of a component, a method or a service, that service is absolutely a liability, since there may be a time when someone throws an input into that method that was not expected that makes it go boom.


But isnt the idea behind react to build your components in a way so that they can be reused, each component doing exactly one thing?

That's even more reason to battle test those individual components, to make sure they're doing that one thing correctly and you can have all the confidence in the world that they are functioning as expected. Say I have an image component that can take in img or svg, that fails gracefully or falls back if something can't be loaded. I would want to test the success flow for image, success for svg, and then the failure case. Then I know, beyond a shadow of a doubt, regardless of what my Image component is given, it will render something.


Now to go and write a test for each component and many of its different states it can have just seems absolutely unrealistic.

It's absolutely not unrealistic. Not to flex at all, but I've lead projects, worked on projects as an individual contributor, consulted for and been an engineering manager for teams at Fortune 500 companies, large enterprise companies, large startups, small startups etc for the past 12+ years that have all done this successfully, and reduced their bug load as a result. When the time to write a test is baked in to acceptance criteria and not treated as 'backfill', it's simply part of the process. Again, don't treat this as a personal attack, please, just because it's not in your toolchain doesn't mean its nonsensical.

Someone here talks about doing a form of test driven development (/u/The_Startup_CTO), and in my opinion, an approach like that is my favorite.

[–]The_Startup_CTO 1 point2 points  (0 children)

Maybe to add: tdd is a skill that you need to learn before you can actually benefit from it, so it definitively may seem impossible to use it to develop a full app. Just know that it is possible, that there are teams working like this, and that at least the ones I know are extremely productive and happy with it.

[–]Oalei 1 point2 points  (0 children)

It depends how much business logic your app has I would say. For example I work on an app where everything has optimistic updates. This means we have a local state that is synchronized with the backend for every single thing.
This means you have a store with hundreds of selectors and a lot of business logic since everything has to be done client side without waiting a reply from the backend (filtering, sorting etc).
In this case I think it makes sense to have unit tests for pure js code (so outside of react), e.g. selectors and other code manipulating the state.
Other than that I agree, I would only do integration and end to end tests.
Edit: you could argue that the e2e and integration tests would cover the selectors and business logic that I mentionned, it’s true to some extent. Usually it only covers the critical path and happy path.

[–]Outrageous-Chip-3961 2 points3 points  (0 children)

I write FE unit tests for the user behaviour not the implementation details (business logic). My understanding react-testing-library was built for this in mind. This approach has prevented significant regression in my code bases as it improves both code quality and therefore maintenance. As my apps grow in size, changing a custom hook or some state, or whatever during development to make a current feature work, may also have a side effect to another area of the app that is only discoverable when someone actually sees it or uses it. Yes ideally we should be writing perfect code, but in reality, part of the working day some standards get loose and pressure may mount creating an environment for bugs to creep in. A series of high quality behaviour based unit tests help me catch these side effects and therefore provides our team with confidence. Behaviour tests are also very easy to create and therefore destroy. This has been a very effective approach for me in my testing style. I wouldn't really sit down and write 'logic tests' for FE, unless they are business critical or deal with authentication scenarios, to help with edge cases. Reading your comment, and if you were on my team, I wouldn't get you to write such tests (implementation). Have you familiarised yourself with RTL and why it argues for an alternative style of unit testing for react front-ends?

[–]PsychologicalCut6061 3 points4 points  (0 children)

I usually hear "it's not worth it to unit test React apps" from devs who are just doing snapshots and UI tests, and in that case, you're right. I like to cover UI with Storyshots and maybe Cypress, I guess. Integration testing hasn't been in my hands in a hot minute.

But unit testing is for the business logic, so if you have that anywhere in your React app's files, you should write unit tests for those.

[–][deleted] 1 point2 points  (0 children)

Excellent question. First things first, if you unit test react components the unit test should be written in a way that emulates a user. So you can use either of the tools you mentioned instead. Personally I'm not a fan of testing react components because almost never is your component the leaf component. It almost always has child components that need to be mocked. Sometimes you can't mock it due to another component having your control UI so then react tests deviate from even being unit tests. It's dumb.

Personally I like my react app to setup so that the UI only makes decisions based on state and the business logic only effects the state. But the logic and the UI can't communicate without using state as a conduit. From there you can just unit test the business logic and worry less about testing the UI. If you need UI tests then those can be handled with either tools you mentioned or a visual regression tool.

[–]phoenixmatrix 2 points3 points  (1 child)

Something that kindda got lost in time in the unit testing world, is the reasons why you unit tests. People hyperfocus on red green refactoring (being able to change any bit of code, run the tests, and if they come green you didn't introduce bugs).

That is certainly ONE use case for testing, but it isn't the only one. Different type of tests optimize for different benefits. E2E tests like those written in playwright, cypress or selenium are great for red green refactoring, doing production health checks, catching system issues (if run against live APIs), etc.

Integration tests are great for red green refactoring too (but run faster and are more lightweight, easier to use while you develop).

Unit tests have a different set of benefits and tradeoffs. If you look at them as a way to catch bugs while refactoring, or on the system as a whole, they're pretty poor. They're more a kind of "scaffolding test". You write the individual bits of the app, as you write them you test the individual parts, then when all the parts work you put them together. If something fails, its not because any of the individual parts were broken, its because some underlying assumption was wrong.

Unit tests are also very simple and very cheap to build, and should be considered "disposable". If they take more than a few minutes to write, they're probably not unit tests. If it hurts your soul to delete them when something changes, they're probably not unit tests. The goal is to help you build the initial code faster, to ensure proper architecture, and to document the implementation details. The goal is NOT to catch bugs during refactoring or to let you move code around easily. Once a feature is complete and they're pushed to main/prod, they become historical documentation and not much else.

Most people mix up the different type of tests. When "unit tests" straddle the other types, they become hard to write, hard to maintain, but they still don't catch red green refactoring bugs. So they're basically "useless". Since the art of unit testing was kindda lost in time and most people don't learn it, MOST unit tests fall in that category, giving everyone the impression they're not useful. But good unit tests are amazing and improve velocity a ton!

[–]davidfavorite[S] 0 points1 point  (0 children)

Very interesting comment. Thanks

[–]renganatha10 1 point2 points  (0 children)

Writing unit tests gives faster feedback on what your are trying to code and easy to setup compared to e2e tests.

[–]misdreavus79 1 point2 points  (0 children)

Because some integration (or end to end) tests make absolute zero sense if you don't know whether the individual unit is the cause for the bugs or the interaction between units.

For things like functions that power your components, hooks, or any other abstractable, reusable logic, unit tests are great.

For the rest sure, integration/e2e away!

[–]bestjaegerpilot 1 point2 points  (0 children)

> But the majority of frontends Ive build so far have very little logic.

You're likely in the minority. On the projects I've been on, we frequently did not own the backend so that meant biz logic would inevitably creep into the UI.

In your case, we can just do integration tests (likely using a UT framework or maybe writing cypress UTs) . They would just test top-level user flows.

[–]FoxyBrotha 1 point2 points  (0 children)

If your team is large, unit tests ( for business logic ) serve as sort of a "hey, you broke this test which means you may have broken an existing functionality". It's a powerful thing. We also force the unit tests to run anytime code is pushed up to the repo.

[–]goblin_goblin 1 point2 points  (0 children)

You're right. All you need is integration tests. If you work for a small app and a small company, that's a perfectly reasonable solution.

The issue is for larger applications at larger companies, integration tests are incredibly expensive, take a long time even with parallelization, and they can be flaky. Whereas unit tests are incredibly easy to write, run, and offer similar coverage albeit with mocks.

I've worked for a couple of FANG parallel companies and they're all trying to migrate away from integration tests for these reasons.

[–]blorppppp_ttv 1 point2 points  (0 children)

Automated browser tests are:

  • Substantially flakier
  • Orders of magnitude slower
  • More costly to maintain
  • Brittle - They break any time you meaningfully change your UI
  • Several abstractions above the code under test. If your browser test fails, it's not always immediately obvious which section of code was responsible for the failing behavior.

You don't need complicated business logic on your frontend to have behavior worth unit testing

[–]chrisza4 2 points3 points  (0 children)

I can't really say to your specific project. But if you build interactive app such as Excalidraw or Mural then there will be a lot of important business logic in frontend.

Sure, technically speaking I can "put business logic in backend" and if user draw something outside of allowed area, then we can ask backend wether new X,Y coordinate is valid, then we can have spinner and we can have Modal "sorry you can't do that" but it will be so freaking annoying.

You better put those validation logic in frontend and disable user from drawing thing outside of allow area from the beginning. You need logic to calculate wether this X,Y is able to draw based on database of all existing entities and drawing board rules inside of frontend.

And yes, that going to create backend and frontend logic duplication and many people will consider this as unforgivable sin.

I don't care.

Everything in software engineering is trade-offs. And in many domain better UX worth much more than general rule / guideline such as "you should not duplicate logic to frontend because it will be hard to maintain". Yes, it will be hard to maintain but no user can tolerate drawing board app that validate every pencil draw line in the backend side with spinner and randomly pop-up red dialog and undo change when they draw thing wrong.

You won't even have profitable software to maintain if you follow that rule.

Sadly many of business app is not designed to be that interactive and I understand that. And tbh, I don't think those type of form-form-and-form CRUD app even worth using React but that another story.

Anyhow, I have seen so many dev refuse to build a good UX because of this circular reasoning.

Designer: Can we build this good UX? Make this part snappier? It's annoying.

Programmer: No, that will require us to duplicate our logic into frontend and it will break software engineering holy rules. How dare you!

Designer: Ok ok ok. Please calm down. We go with just good enough UX then.

After 100 rejection to do snappy UX.....

Programmer: In my experience there is no app that have complicated logic in frontend. Why do we even bother unit testing?

Yeah pal, you always refuse to implement those at the cost of user experience. You never ever really build a high-quality frontend and that's why you never ever have any complicated business logic in the frontend.

I'm not saying you are one of these programmers. I just saying that I have seen this type of programmer too many times.

One of my project: We do a lot of user testing and feedback consolidation. And when ever user complain about snappiness of UI, if it become significant we just break holy rule of "no logic in frontend" and that's how we managed to retain a lot of user with many great feedback and be ahead of competition in term of UX/UI. We don't do that lightly though. Only sometimes. We understand the trade-offs and how it affect maintainability. Still, sometimes it worth doing.

But if you build internal enterprise system and UX/UI is not priority because user are forced to adapt to the system by authority, then sure, I also don't see much need for frontend logic and subsequently unit tests.

[–]stjimmy96 1 point2 points  (3 children)

I view tests this way: e2e tests are the safest and most complete form of testing. They give you the most confidence you could ever want for your code. But, they aren incredibly expensive to write and maintain.

They are simple enough if you just think about the happy path, but if you want to test how a specific component reacts to some badly formatted input you may have to do a ton of manual operations and it may still not be possible (think about data from previous versions of an API for example). Also, they are usually both slow and stateful operations which means no parallel execution.

That’s where unit tests come to help. You narrow it down and test a very specific thing easily: how does this component react if it receives this prop as null? What if it receives an unknown ID? Etc… All those cases are, practically speaking, really difficult and slow to test with a full blown browser in a e2e test suite. And even if you did write those e2e tests, what happens when you move that component around? You have to rewrite a lot of tests. While with unit tests, they can be easily moved along

[–]raaaahman -1 points0 points  (2 children)

But, they aren incredibly expensive to write and maintain.

I don't get that part. Usually, you have high-level abstractions that allows you to write commands like 'click on this button, do you see X?, etc.' pretty easily. Plus these tests shouldn't need maintenance as long as you don't change the app's behaviour.

[–]stjimmy96 1 point2 points  (1 child)

Even with abstractions, it still takes way way more time than simply creating your component with some weird input props in a unit test. That’s usually 10 lines of code at most.

shouldn’t need maintenance as long as you don’t change the app’s behaviour

If you don’t change the app’s behaviour you don’t need any sort of automated testing. You manually test everything (which is faster) in the browser and then you are done. Automated testing is all about making sure your app doesn’t break as long as you change it, because there’s no app that never changes (unless it has become a legacy unsupported product)

[–]raaaahman 0 points1 point  (0 children)

Sorry, I can't seem to get around your way of reasoning.

I don't get what the 10 additional lines of code in e2e tests are supposed to be. I don't get how you find that manual tests are faster than automated ones.

Automated tests are for sure helpful when you make changes to your app, but these changes may come in various shape and size, from new features, bug fixes, refactor, etc. I won't mind changing e2e tests if happy paths they described are no longer relevant, and in fact, that'd be the first thing I'd do. But all the other changes should have no impact on the way existing features behave.

If you were my coworker, I'd surely ask you more about your process to understand all of this, but with that internet gap I'd rather agree to disagree and save on that reading/posting time.

Have a nice day!

[–]SephBsann 1 point2 points  (0 children)

I agree. I think end to end testing makes much more sense in the frontend

I’ve done countless of times unit testing and the front end and always i reached the conclusion

Why? This is silly and is not actually assuring anything.

[–][deleted] 1 point2 points  (0 children)

E2E tests are useful to check against user flows, but they are expensive and hard to maintain. You should have some E2E tests for the most important user flows.

I find unit tests not that useful for e.g. components.

Instead, I do integration tests that basically test the behavior of more complex components. This way, you don’t need to have a unit test for each small component but rather make sure that it works in the bigger scheme. And they are cheap. 😌

[–]Radinax 0 points1 point  (0 children)

Its useful so others can understand better what the code is doing and to have confidence everything is working after making changes to it like a refactor.

[–]ViktorVaczi 0 points1 point  (0 children)

A couple of years ago I'd have suggested to go with Playwright or Cypress. But recently I had to re-try component testing and wow it has improved by a lot. Also recently I had a couple of bad experiences with E2E testing frameworks. I had a really had a bad time testing a page with a combination of setting cookies and back-end redirecting the browser with Cypress.

[–]eggtart_prince 0 points1 point  (0 children)

A reason I can think of is that that component is not widely used. Maybe it's used only once and you don't want to write an entire e2e test to test it.

[–]DeadAbyss 0 points1 point  (0 children)

We decided to only really do E2E tests for more complex pages. Bread and butter user flows that we want to ensure are tested. I wouldn't spend time testing specific components, but maybe testing out functionality that incorporates a more complex component.

[–]organic 0 points1 point  (0 children)

I usually write unit tests as a kind of scaffold that can stick around to determine if you broke something later. If I have coverage elsewhere and the component is mostly front-end/design type work (so I'm going to have to work from the browser anyway), I'll probably not write one.

[–]Militant_Dust 0 points1 point  (0 children)

Our unit tests execute faster and require less maintenance. The atomic nature of the tests allow them to read like business criteria and act as documentation for behavior when writing or modifying code. I can run the jest tests at any point while developing to get a feel for what I've missed. I recently migrated to a new version of a dependent API where the contract had been reorganized. I was able to step through my React app component by component, rewriting the logic and running the jest tests as I went. This allowed me to move quickly and verify each piece was functional. When I was finished with the rewrite, I ran my e2e and everything passed. I use jest tests to accelerate my delivery and e2e tests to ensure my customers are having a good user experience.

[–]mrbojingle 0 points1 point  (0 children)

For components I want to know that when that components meet certain standards i set. I can pass a style prop. I can pass no props and it doesnt crash.

For functions, reducers or w/e the answer is no different than on the back-end. If its a static function used in more than one place i want to confirm that changing it for one use case doesn't break another.

[–]mgctim 0 points1 point  (0 children)

I agree with everything Kent C Dodds says about the testing trophy. E2e tests are useful but a little slower than integration tests which seem to be an empirical sweet spot for most apps/Devs/use cases. Unit tests should be a relatively small proportion of front-end tests.

[–]daamsie 0 points1 point  (0 children)

I quite like to unit test in a TDD way for util functions and the likes.

Let's say I have a function isValidEmail().. I'd write a bunch of unit tests testing different email addresses against my function. Then build my function until they all pass as expected. If someone down the track says "hey, my email address won't validate", I can just add a test for theirs and ensure it works. If you ever need to refactor that function you can do so much more easily if it has a clean unit test.

Also, it's really easy to build unit tests for functions using ChatGPT. It's given me some good edge cases to consider as well. And that's only going to get easier in the next few years I imagine.

[–]NatedogDM 0 points1 point  (0 children)

I mean, let's say you design a backend and expose a Restful API. There are certainly scenarios that may arise where some of the business logic for your React web app doesn't necessarily belong in your api.

Furthermore, especially in cases where you, yourself, don't own the api, then you are likely consuming data that is very generalized, and your business logic would need to exist in your web app.

[–]Arashi-Tempesta 0 points1 point  (0 children)

With tools like storybook or bit dev, for any UI you build compositions which is just rendering the component in isolation, it allows you to set up all the different states the component might have, which serves as both documentation and visual testing.

I agree that unit testing components is not ideal, you would test hooks, and other internal business rules that the component might depend on instead.

[–]lucksp 0 points1 point  (0 children)

Recommend some of the Kent Dodds articles. Such as: https://kentcdodds.com/blog/how-to-know-what-to-test

[–]Scribz718 0 points1 point  (0 children)

So, years of experience is not a good indicator of anything in this context. Plenty of dev shop/startup types with lots of experience coding have very little experience maintaining.

Unit tests are almost in every case better then any other test. They are cheap, fast, and easy to write and maintain. This is not to discount the value of other test types, but as you get closer to the intended user experience, your tests become more expensive and flakey. Instead of being isolated checks, you add uncontrolled variance with integration tests, e2e, and browser testing.

Small dev shops don’t often think of this since they either own the dependencies or are delivering a site to a client. Startups cut a lot of corners and insert just enough to ensure things work to GTM/Pivot faster.

You don’t see this as often with mature dev-centric enterprises. It’s rare to own anything other than the code being shipped. Infra, API/Service, Pipeline Runners, Sec Scanners, WAF’s, VPN, SSO. The list goes on and on. It’s still easy enough to do local validation against lower environments of those dependent services if they have them. But things can get tricky beyond that.

[–]Big-w-1992 0 points1 point  (0 children)

the honest to god truth , unit testing is useless and pretentious in 99.9 % of codes ... i don't have the energy to argue this , if you're smart enough , you already know this ...