Here's what I do. Apologies for the wall of text, feel free to skip if TLDR.
I implement and test algorithms in fully hosted environments (i.e. running under an OS on full-featured computers, almost always Linux in my case), individually ("in units"). I archive them one per directory, with a README describing the thing, and any test/example executable showing short usage when run with a single -h or --help command-line argument. For more complicated stuff, I often have images and data in the same directory too.
For complex data structures like trees, heaps, directed graphs, etc., these test and example programs often output data structure information in Graphviz format (
DOT language), for easy visual verification.
The ones I write in C or C++ for use in microcontrollers, are
never in library form, because I always end up adapting them for the use case at hand. So, instead of focusing on the same code being able to handle all situations I can imagine, I write them to do one thing, in as simple (but robust/secure/non-buggy) form as possible, so that it is maximally adaptable to my future needs, whatever they may be.
When integrating various pieces into a functional firmware, I use a mixed approach: not purely top-down or bottom-up, but a mix of the two. At this phase, I consider the various pieces via their requirements, inputs, and outputs, and piece it together like an interwoven puzzle, and often develop the underlying structural "scaffold" code in tandem or slightly ahead of the functional units I add – by "scaffold" I mean things like bus interfaces, interrupt handlers, with temporary timing and memory use measurements to make sure I do not unexpectedly run out of computing resources. When there are details I am unsure of, for example using a single timer for two different purposes, or maybe examining the computational cost of a specific approach, I do write test firmwares (often in the Arduino environment for rapid testing) to examine that case and that case only, without anything else. These too go into the same archive of tests and examples. (I do have thousands of these already, but only keep the most interesting/useful few hundred on my work machine, and move the rest to offline storage.)
Whenever I have added a functional piece, I do test the whole in practice, concentrating on the edge cases. For example, with button interfaces I test contact bounce (brushing two ends of multistrand wires is a pretty good stress test) and multiple simultaneous keypresses. I never proceed while there are misbehaving edge cases, but some of my colleagues and former colleagues disagree with that, because they believe getting the product on the market is more important than having it behave exactly as desired. I understand their viewpoint, but that is just not how I develop stuff.
I have also created simulation test benches I can run on fully-features OSes, before implementing it on the hardware. This is particularly useful for GUI development, where one can use GTK or QT (or any similar toolkit, or even SDL) for the UI, with mouse clicks as touches on the touch screen (with randomness added to model limited resolution), and key presses to simulate physical buttons. (For pure mockups, to illustrate intended final product interface without reusing any of the code, I recommend HTML + CSS + JS, so that it can be demonstrated on any device with a standard web browser. This is especially useful for touch-based interfaces, since you can get a real physical feel for the UI by using e.g. a phone for the demonstration. I also like to use local/serverless HTML+CSS+JS "tool pages", because JS is pretty darned well optimized in current browsers, and such tool pages work everywhere.)
During the development, I like to keep a diary of sorts, listing any interesting or frustrating bits. This is a very recent addition to my workflow, in the last year or two, after almost three decades of paid professional software development. I "stole" this from the diaries of historical explorers, as I saw it an excellent way of self-reflection and analysis of my own performance afterwards, something I've always struggled with. You don't need to mention it to anybody else, especially your employers, if you don't want to; but it is an excellent tool to work on ones time estimates, for example. (My own time estimates have always been off the mark: most often underestimating the time needed, but sometimes overestimating [when I forget checking my test case archive before making the estimate].)
Finally, one of my favourite real world tests is just describing a problem, then telling an unsuspecting
victim tester that this gadget can solve it, and then see how they (try to) use the gadget to solve the issue. I try not to intervene, although sometimes a couple of sentences outlining the idea of how I'd find the solution may be necessary. Theory gives us possibilities, and is therefore very useful (and I do still read peer-reviewed papers in physics and cs, even though I'm no longer in academia), but practical reality rules.