3 ms·
A few more in my experience: Database abstractability. Here I don't mean "type of database" like Postgres vs. MySQL vs. Mongo, but making sure in your system y
by jerf 4y ago
A few more in my experience:
Database abstractability. Here I don't mean "type of database" like Postgres vs. MySQL vs. Mongo, but making sure in your system you have the ability to target and isolate the entire DB. Integration testing is both vital and slow; as you grow you will be glad you have the ability to run multiple of them at once because they don't interact. With MySQL & Postgres this can be as easy as making sure you code takes the database name as a parameter instead of hard coding it. If your database lacks this feature, take the time to abstract out table names so you can use a certain prefix/suffix for a given test.
If you can't quickly recreate the schema and populate with some known set of test data, take the time to write code that will take an existing DB and truncate it back to start or whatever. You need those integration tests, and you need them to not take two hours to run. Get that going at the beginning. I've not yet gotten to the point I need prepopulated schema which is handed out by a central manager, and then the cleanup is run out of the actual test cycle, but I'd be willing to go that far. In 2022 it is likely not a problem at all to have literally a thousand copies of your schema lying around on some test DB server, just to make the tests run fast. Use some versioning solution to make sure they're the right version.
There is a LOT of variation in databases here; you will need to tune your process to your DB. In some, schema creation is wicked fast and you can just write all your tests to write out new ones without a problem. In others it is slow but very parallelizable. In others it may be just plain slow, in which case the code to take existing schema and reset them is more valuable. In yet others truncation may be difficult and/or slow due to loose serialization requirements. Do whatever it takes to make your integration tests fast, or at least, as fast as possible. Do not just settle for even simple tests taking 10 minutes because you naively do a slow thing. You really need these to go fast.
Authentication and authorization; the earlier you work in some solution, the happier you will be. Sometimes this is easy to retrofit, sometimes it really isn't. Even if you start with authentication and a list of who can do what in the very early phases it'll still prevent you from accidentally depending on having no auth at all.
Write your code to be thread/async safe from the beginning, even if you have no use for it yet. You don't want to be in a position where you need it but you've got a 2-year-old code base now built on the deep assumption of single-threading, and it only gets worse as the code base gets larger. It's a slight marginal expense to keep it in mind, it's a project-destroying nightmare to add it back in later.
In static languages, get the types declared as early as possible. Get away from (int, int, int, string, string, bool, bool) function signatures as quickly as you can. It's not exactly impossible to retrofit better types later, but it's at the very least a pain in the bum disproportional to the cost of having declared them correctly in the first place. Even if your first stab at real types is wrong, it's still easier to fix them this way because you have the type name as a hook for your IDE, or in the worst case, for sheer grep-based refactoring. (Even in dynamic languages I'd recommend this, but they have more options and the analysis is more complicated. Still, I'd suggest having a "user" that knows how to render itself as a string is better than passing around the user's name or something as a string everywhere and expecting every bit of code that needs to do some user-object-method-y thing to have to look it up themselves.)
- schwartzworld 4y ago> Still, I'd suggest having a "user" that knows how to render itself as a string is better than passing around the user's name or something as a string everywhere and expecting every bit of code that needs to do some user-object-method-y thing to have to look it up themselves. For some reason this seems to be the best kept secret in software development. Type-driven development lives somewhere at the intersection of OOP and FP. Here on HN every time TypeScript comes up, people claim to prefer code comments or some other nonsense that doesn't give you any of the benefits, because they see types as (userName: string, userAge: number) and not User { name: string, age: number }
- kroltan 4y agoYes, type-driven development saves so much hassle, both in ingesting user-provided data (parsing request bodies etc) as well as limiting the API surface of what you can do to things. Sadly, one very useful feature is missing from most languages, and that is, the ability to make opaque or partially transparent wrapper types. For example in C# before record types, you'd need about 30 lines of boilerplate if you wanted to make a wrapper for an identifier that can only be compared for equality. Wrapper and rich types allow writing shared behaviours for those specific kinds of information, and greatly reduce the "accidental API surface" of primitive types that plagues us with so many bugs and data leaks.