Discourse in software development and programming I believe has long suffered from an issue of not being aware of the problem domain one is talking about. Too often I see the presumption that if you are writing code then you are simply writing code, nothing more. Graphics code for a game, backend code for a REST API, or code for any other problem domain all get condensed together into one category in people’s minds. Then when someone comes up with some novel theory or perspective of development suited to one particular problem domain, it is presumed to be universal. When rarely is that the case. For example, the type of mentality and perspective you’d have when writing graphics code to be as efficient as possible in a resource constrained envrionment is not the mentality you’d have when writing code to route HTTP requests on large fast servers.
I see contention arise between low-level code, higher-level user space application code, and backend code, but within each of those are even more subcategories of distinct problems domains.
In recent times I have seen confusion stem from people trying to naively apply mentalities from backend enterprise development to performance-oriented client code.
One prominent example that comes to my mind is the subject of SOLID in Unity mobile games. SOLID being something birthed out of the backend enterprise crowd as a highly opiniated approach to architecture that tends to add many layers of abstraction and overhead for the principles it espouses. That can turn into an issue when your problem domain does not rest on high-grade servers with liberal operating budgets, but is instead a mid-spec mobile phone trying run a realtime 3D game. This has led to multiple variations of ‘Dependency Injecton Frameworks’ that rely on slow features such a Reflection that can add inhibiting overhead to simple operations like object instation. All done in the name of the ‘Dependency Inversion Principle’, which is Okay in problem domains where you aren’t in such a resource constrained environment, but not so much on a mobile device trying to run realtime 3D game. If you are writing C# to run high-performance, and real-time, on a mobile device you generally need to become highly pedantic about performance. Preferring approaches that are dead simple with the least amount of overhead possible. Like a singleton.
Another one is Test Driven Development. The premise that you first write unit tests before writing applications. This is spawned out of problem domains where your expected output can be clearly defined up front. A prominent one is writing REST APIs. You can realistically write out tests for a REST API up front, generally it a set of CRUD operations and you know what you want requests to look like, and you know what you want the responses to look like. The input and output are clearly definable from the very begining. In this problem domain it makes a lot of sense to write test up front explicitly for the output which you expect.
However let’s flip to a different problem. Getting the “feel” of the player in a third person game correct. So when you push on the joystick the character runs forward and turns in a certain way, with a certain weight and responsiveness to satisfy a design instinct. So when you jump, you appropriately grab onto ledges, or flip in the air. The expected output in this problem domain is more elusive, more subjective. It is not something you can explicitly define akin to the response from a database. This does require someone to sit and fiddle with it, for a long period of time, testing it out, adjusting little things here and there, until it “feels” right. Until this has been fiddled with enough, and sorted out, the programmer implementing it may discover they need to take multiple different approaches in the implementation. Completely deleting certain sections of code, or refactoring a large surface area. The end objective is not an exact testable response, it is the satisfying of a design instinct. Pre-emptively writing tests could end up testing completely irrelevant things, or worse, just sucking up time or making it harder to refactor and change code as necessary. This is why gameplay logic like this tends to not be done in a test driven development manner. It may not have any tests at all. More commonly you may find what are called integraiton tests, which are not granular test, but instead test a large surface area of code. Such as creating mock joystick input to make the character walk forward 10 steps, then checking to see if they moved. It’s not specific, it tests many systems, but it does ensure nothing broke.
There are also certain things in this third person game context that you could test. Such as ensuring some new input devices properly produces the expected outputs. Or perhaps serializing and desrializing a save game. There are specific problems in such a game which do have outputs you can expect up front, but not always.
There are also problem domains where the output could be potentially defined, but you have no way to exactly generate that output to write a test before-hand. This can occur in graphics code, or shader code, frequently. Where you are implementing some alghorithm to render something in a particular way. You do have a known expected output in terms of what you want it to render, but the exact data representation of that is a chunk of binary data from an image. Perhaps if you are exactly trying to match some pre-existing image you can define the test up front, computer graphics researchers have done this with a photo of the cornell box for global illumination, but that’s a rare occurance. You also don’t have any clear idea of what exactly the data input is going to be. Graphics and shaders require a lot of fiddling with buffers and data layout, expirimentaiton, to arrive at an optimal solution. This tends to be a problem where, although it does have an expected explicit output, you can rarely can make a test up front. You have to figure it out first, get it working to a certain baseline, then perhaps once you have a good grip on the implementation you generate the data to produce some test to ensure it doesn’t break.
Personally I’d argue most programming tasks fall into the prior description where you you don’t fully know the expected input and output. It requires a bit of discovery. It is not common to explicitly know both input and output up front, then be able to generate both up front to pre-emptively make a test. This is it also common in the programming industry to first implement something then make a handful of tests afterwards to ensure it doesn’t break. I believe others naively, of disingenuously, try to label that approach as some kind of problem when often it is the only way to do it.
This also has overlap with the subject of granular testing versus broad integration testing. It is also typically suggested that exhaustive, granular, unit tests on as much as possible is what you want. I would say this depends on the problem domain too. Creating a test solidifies, and entrenches an implementation, and you want to make sure what you are solidifying is what you want to remain solid. In some problem domains the implementation details are not important, and could be refactored multiple times. It is important that a test ends up testing only what you want to be solidified, the expected input and the expected output. You don’t want to soldify potentially transient implementation details between expected inputs and outputs, as all that will do is solidify code that should be refactored. There are some problems where nearly every method is critical, and you do want it all to be solidified. But there are problem domains where multiple methods to entire classes are just an implementation detail. In such cases some kind of integration test which ensures the implementation doesn’t break can be good, but you wouldn’t to make exhaustive granular tests to solidify what is a transient implementation detail as it will create greater diffuculty to refactor it in the future. Tests should declare what should not change in a codebase.
There are many problem domains I can highlight where writing tests up front, or writing granular unit tests, is an absurd notion. Yet despite this, I cannot count the number times I still regularly encounter someone suggesting Test Driven Development with no regard for the given problem domain.
Test Driven Development only works in problem domains where before-hand you explicitly know the exact input of data you will need, and you explicitly know the exact output of data you will have, and you can reasonably generate a mock representation of that data beforehand. The obvious example is a REST API with CRUD operations on a database. You can define the API call, the JSON blobs you will feed as input and the exact JSON you expect back. But that is one specifc problem. Test Driven Development can apply to many problem domains, but Test Driven Development isn’t an universal approach. It’s viability depends heavily on the problem domain.