6 ms·
The solution is to be religious about testing and writing tests. If you've done it well, refactors are pretty safe and your objects are pretty well understood.
by phaedryx 4y ago
The solution is to be religious about testing and writing tests. If you've done it well, refactors are pretty safe and your objects are pretty well understood.
- pantulis 4y agoAlso, you need to be religious about testing and writing tests _anyway_.
- ajmurmann 4y agoAs someone who spent a good chunk of their career teaching TDD, I agree and disagree. Yes, tests are crucial. However, which tests to write varies among other things by language. When I write Ruby, I benefit from testing even the simplest things. When I write Rust or even Type Script or Swift I'll focus much more on tests for complex logic and on integration and acceptance tests. Static typing eliminates an entire, larger category of issue I need to address with tests.
- pantulis 4y agoSure! But you need to be a testing practitioner anyway. I agree that perhaps the number of tests is different, but the teams processes and cultures should already be there. But to counter myself, the perceived lack of developer speed and flexiblity of a strongly typed language (with Rust, I always get the feeling that I'm fighting with the compiler!) is also solved in the long run with practice and tooling.
- vidarh 4y agoWhen I write Ruby, I test expected behaviours, and if there are type errors in there those tends to fall out from tests I needed anyway. If you need to test specifically for errors due types, then generally that suggests that either your application does not normally exercise those code paths at all and/or you fail to test behaviours of your application.
- Yoric 4y agoDoes this include writing libraries for use by third-parties?
- vidarh 4y agoTo an extent. In libraries for third parties you do need to test contracts specified by your docs, but a well written Ruby library should intentionally avoid over-testing typing (both in separate tests and in code) and focus on testing behaviours. Sure, test for sane failure modes in line with the documented contract, and that may include the occasional test that is de facto a type test, but often testing for types, especially in languages with poor type systems, but also in Ruby where we have alternatives, ends up with tests for classes which is frequently the wrong thing. E.g. in Ruby never, ever check for somearg.kind_of?(IO) if all you ever do is somearg.read - if you absolutely must typecheck somearg, the Ruby way is to check for presence of "#read", e.g. somearg.respond_to?(:read), or try and fail responsibly (and often just allowing the NoMethodError to bubble up is the right way to fail). Also think it's just fine for people to add and ship Sorbet type declarations for gems etc. to then signal those contracts so people can verify them if they choose. There's no reason not to offer that when it's reasonable to do so.
- Yoric 4y agoI have zero to no experience coding in Ruby, so I cannot comment in that case. In Python or JavaScript/TypeScript, though, my experience is that failing to validate types at the borders (i.e. every single function/method/constructor/generator/... exposed in your API) is pretty much guaranteed to end up, months later, with developers attempting to sherlock out surprising breakages in production from logs that make no sense and traces that do not show anything remotely close to the actual culprit. I have the scars to show it :) Of course, YMMV.
- vidarh 4y agoThe "Ruby way" in this respect tends to be to avoid being over-prescriptive. That doesn't mean "do no checks", but "do only the checks actually needed" with a very different expectation of "what is needed" than in many other languages. Hence don't check for an IO object when what you care about is the presence of a "#read" method. Or don't check if something is an Array if what matters is that it supports "#map". Instead, either somearg.respond_to?(:map), or if it needs more than map, somearg.respond_to?(Enumerable) (this seems broad, but supporting Enumerable only requires implementing "#each" and including the module, so a caller "worst case" can reopen a class or wrap their object), or call one of Array(somearg) (tries "#to_ary" then "#to_a" then falls back to returning [somearg]) or Array.try_convert(somearg) (tries "#to_ary" then falls back on nil). Consistently picking the most generic applicable options when faced with choices like that still enforces the contract on the boundary, but also tends to lead to code with much less ceremony. E.g. far fewer [something]Adapter classes, or glue code to convert data before calling methods that are being overly prescriptive. But for most statically typed languages, when people talk about static typing, they still talk about a class and a type as interchangeable (there are exceptions, and it's getting better, and with increased type inference coupled with increased support for type annotations and analysis of dynamic languages, I expect there to be an increasing convergence, though).
- lmm 4y agoYou really don't. You need to test the business logic, but for the "plumbing" (which is often 90%+ of the code) often if you have a decent type system and use it effectively there's only one way to write it that would compile.
- Salgat 4y agoThe point is that compilation automates all this. It's just one more failure point you have to devote man hours to.
- ptnxlo 4y agoCompilation won't tell you whether your code follows the business logic it's intended to do. You do need to rely on tests even if the implementation of the programming language compiles it or not.
- Yoric 4y agoActually, very often, with a strongly, statically-typed language, in a well-designed API, it actually can. Unfortunately, most people got a taste of static typing with Java and the initial language and library design were done in such a way that types were well, if not entirely useless, then certainly under-used. But if you look at many libraries for languages such as Rust, Scala, OCaml, Haskell, F#, ... (I haven't looked at Java in a while, but I don't hold high hopes on this specific front) you'll find many examples in which the API and the type system cooperate to guarantee that high-level protocols are enforced. That being said, I absolutely agree that strong static typing does not mean that you don't need tests. As everything, if you want to be safe, you need a defense in depth, with good API design and many test layers.
- Salgat 4y agoI'm not talking about business logic tests, and never implied that compilation removes the need for them.
- hbrn 4y agoBoth religions are leading you astray. The solution is to become a software atheist and just break things down when they no longer fit in your head. Interestingly, this seems to be what microservices originally were about until they got twisted by hype. It is also what OOP was about, until it got twisted by another religion.