Can anyone elaborate on this?
Can anyone elaborate on this?
- Followup to the above: because so much of the "work" involved with UI code involves interacting with another API, usage of that API tends to spread itself through the entire codebase like cancer. You can create a careful separation of concerns, but it takes a lot of upfront work, verbose boilerplate code, and constant vigilance -- if you're just handed an existing UI codebase you won't have that luxury.
- UI code is highly stateful and highly mutable. A core skill in UI programming is managing your app's enormous state profile. Single-path, whole-world rendering techniques like React/other shadow DOM techniques really help here.
- UI is highly temporal. It needs to change over time. Testing anything to do with time is an enormous headache. Animations can force you to temporarily separate your underlying state from its representation, which is a testing nightmare. But just in general, writing unit tests that involve time passing is infuriating and fragile (even if you can control time passing via a mocked clock object).
- A lot of core functionality in UI involves different pieces handing off to each other -- one window transitions to another window, etc. To test that properly you need integration tests, but now your integration tests also need to interact with the underlying OS/browser (because you can't separate any of that stuff out). So your integration tests are in general way flakier than you can get on UI-less systems that really only need to worry about faking out a DB of some kind.
Take spacing: if something moves 200px it's probably broken. But a couple pixels off are usually fine. Unless those 2px are the vertical position of an icon inside a button or two things that need to align. Where do you draw the line? Humans are pretty good at spotting when things look off just by looking at a screen. Computers, not so much (yet).
Unit testing frontends is hard because to create a test case, you must simulate a human user's behavior (clicks/taps on a UI) which is inherently complex, and has a lot of possible variations, and then test the correctness of the response the system displays as a human would perceive it. Just because the system sent back the correct data doesn't mean it was rendered in a way that the user can see/understand.
With non-user-facing systems, you can just send the input, and test computationally that the output fits the expected value or range of correct responses.
Of course, one could create a vision system that tested the response of a UI for correctness, but that would be ... super-hard.
It's more a problem that iterating/debugging/testing is generally so much harder for front-end, however you do it and regardless of whether you're dogmatic about unit testing or not.
Focusing on the difficulty of unit testing is simply focusing on one of the symptoms rather than the root cause.
Like in backend code, I can back out of a method, rollback to the start of a call and re-run it all again when debugging. In front-end code, that's simply not possible because there were a bunch of user interactions and you can't re-run them, you have to restart and click, click, click.
Additionally separate your components logic from rendering and test that; e.g. a calendar widget I just created has a load of tested functionality for creating lists of dates with various attributes (isCurrentMonth, isToday, isSelectable, isSelected, etc). This can be unit tested just fine.
Finally I’d strongly recommend cypress for doing e2e testing.
However, after spending some time with it, literally everything else now feels dramatically inferior.
First, why do many people think that unit testing UI is difficult? How do you unit test UI? There are a couple of schools of thought about "unit tests". A very prevalent one is that you write a "unit" (usually a class) and then you stress test the interface. Often this is coupled with faking/mocking collaborators because we are only interested in testing the interface.
This is kind of problematic with UI code because imagine that I have a class that is derived from a dialog box. How do I test (for example) that the desired functionality is run when I hit the "OK" button? The problem is that the "OK" button is often not a programmable interface on my dialog box. Someone clicks on the button, whatever UI framework I'm using takes over and then magically some callback (hopefully) get's called. I can (sometimes) hack into the depths of my framework and maybe mock the callback to make sure it gets called when a button is pressed, but usually frameworks are not built to accommodate this kind of work. I can also use some kind of UI simulator to press buttons and that would work OK. But I'm going to back up here and suggest that this is not a unit test. It's an automated regression test.
Part of the problem is the framework is "hiding" the internal functioning of the UI. I can't test the OK button, because I just can't get access to it. For me this is a big hint that "I'm doing it wrong". In fact, I don't subscribe to the common view that "unit testing" is about testing the interfaces on a class -- in other words, black box testing. Unit tests are specifically different than other kinds of test: they are white box tests.
I've described this a few times and have yet to find a way that works particularly well, so forgive me if I fail yet again (you can happily walk away with only the first half of this message :-) ). Imagine that instead of "testing", we simply want to insert "probes" into our code. The probes will measure the operation of the code in a specific place and warn us if something is outside of our expectations.
For example, imagine cooking a roast beef (sorry if you are vegetarian -- you can imagine some other kind of roast). We want to cook the roast to a certain internal temperature. There are many ways to do it. A "black box" approach would be to weigh the roast, measure the temperature in the oven and then time how long we are cooking. We can then derive the probable internal temperature from these external parameters.
The "white box" approach would be to stick a thermal probe (thermometer) into the roast and measure the temperature directly. This allows us to see exactly what's happening on the inside, but at the cost of violating the roast's encapsulation.
My opinion is that "unit testing" is this latter kind of testing. We want to expose the internal state of things and to measure it directly -- rather than deriving what we assume the state to be from some defined interface. I furthermore believe that "TDD" refers to the systematic act of finding appropriate internal state and exposing it. Test first is a good way to do that because you end up probing the state before you have written any interfaces -- it forces you to open up that internal state. There are other ways to do it, though.
All of that to say that UI code is not actually any different than any other code. Our difficulty is not that the events driving the system are generated by a user. Our difficulty is that the frameworks hide the internals and stop you from writing unit tests (by my definition). This happens frequently when you are trying to TDD legacy code. The legacy code was not written in a way that allows you to write unit test -- you are forced to write integration tests because you don't have access to the internals. This is exactly the same with most UI frameworks -- they were not written to allow unit testing. And since you can't (or really don't want to) start refactoring the framework, you are stuck.
Having said that, I have found some frameworks to be amenable to unit testing. For me, the best I've found in the web world is React. Interestingly, though, I do not use the React testing framework because it is not amenable to unit testing (by my definition). I build my own.
Hope you found that interesting!
This has further implications, like over-abstraction (all those dependencies need to be parameterized, all those classes need to be interfaces, everything must be indirected so it can be probed), namespace pollution (instead of a cohesive external API, you get a big bag of Lego pieces and have to pick and choose the right combination), brittle design (every unit boundary gets tested on both sides, tripling the cost of modification), etc.
I'm trying to imagine what you are thinking of, but I'm having trouble so it's hard to rebut your points.
Choose almost any moderately popular jar you like from Maven and I could point out the pathologies. Strong smells when you have lots of interfaces with a single implementation, lots of construction indirected through factories (or factory factories), etc.