3 ms·
This is a great question. The answer is that it depends on what you mean by "unit test". I will actually state that it is not hard to unit test front-end (or
by mikekchar 9y ago
This is a great question. The answer is that it depends on what you mean by "unit test". I will actually state that it is not hard to unit test front-end (or more generally UI) code, but I think that the reason for my differing answer is far more interesting than the answer itself.
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!
- barrkel 9y agoYou successfully described why I think unit testing can be harmful to good code - forcing exposure of too many internal details. 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.
- mikekchar 9y agoI think you are imagining something very different than what I am describing. Exposure of state simply allows you to decompose your structures -- which allows you to compose them again. This is exactly what you do in FP. Testing at branch points means that you write 2 tests. If you have n branch points it means 2n tests. If you test n branch points down, you need n^2 tests. You don't test functionality at every level -- because these are probes not tests. Your external API is irrelevant to its construction, which is the point of encapsulation. I can write internals that can be probed, or I can decide not to. But how you use it from the outside is not related. You can build as many layers of this as you want -- in the normal way. I'm trying to imagine what you are thinking of, but I'm having trouble so it's hard to rebut your points.
- barrkel 9y agoI'm thinking of the dominant paradigm in Java OO from about 2005 to 2015 or so. 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.