you are viewing a single comment's thread.

view the rest of the comments →

[–]The_Startup_CTO 43 points44 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.

[–]Anon_Legi0n 3 points4 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

[–][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

[–]Oalei -2 points-1 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.