Not needing types is another benefit of good testing.
That's my experience, at least.
What would you be testing for, exactly? That `SomeClass#some_method` raises a `NoMethodError` when you pass it the wrong thing?
You could test for that, but I don't think that would be a good use of code or your time. If A is passing the wrong things to B, your specs will fail anyway on that `NoMethodError` once B tries to actually do something with those improper argument(s).
I suppose it could be useful in cases where you'd stubbed out B in your specs, but ehhhh.
You're talking like that's rare and not something people constantly do when writing tests in Ruby.
The presumption is that we're talking about a well rounded test suite with both unit tests and integration tests, and the integration tests would indeed be catching the fact that A is passing entirely the wrong thing to B.
Of course, this does mean we're leaning heavily on the test suite here. But, in any nontrivial application I hope that we would have a robust test suite, right? Otherwise we're going to have some problems whether we're statically or dynamically typed.
Also, what happens when you don't get a NoMethodError? Duck typing is an extremely common practice in Ruby, which means that you can easily run into situations where code "runs" but the output is nonsensical.
In my experience this here (usually some
method unexpectedly getting passed `nil`)
is the single biggest class of bugs in
production Ruby applications.
Right, but how do tests help you here?Your specs aren't passing those unexpected nils, and if they were, your specs would fail on the resulting NoMethodError.
Static analysis can find these potential problems in a statically typed language - I miss the days when Resharper would let me know about potential nulls/nils. That's a strong argument for static typing. But, this particular comment thread is about addressing this class of bug in Ruby via tests, and my vote would be generally no.
Duck typing is an extremely common practice
in Ruby, which means that you can easily run
into situations where code "runs" but the output
is nonsensical.
I've been working with Ruby full time for years (admittedly, mostly boring CRUD Rails apps) and I've just never found this to be a problem. It obviously can happen, but I just don't see those name collisions ever happening.I'm pretty sure I started the comment thread ;).
I think there's a couple of scenarios when a function gets some rogue input (e.g., a string instead of an integer) which then triggers a NoMethodError. I agree that tests don't really help you here - what are you testing, that your method correctly throws a NoMethodError? That being said, to combat this issue you often see dynamically typed codebases littered with scattershot validation, i.e. fail gracefully if you somehow wind up with an unexpected input. And then you often write tests for that. Static typing often obviates these tests, because presumably the rogue input has already been handled further up the call stack. Stubbing is the other issue; I think it happens much more frequently than you suggest that A is tested (stubbing B) and B is tested in isolation; or perhaps there's only one test of A which doesn't stub B, and that test doesn't happen to trigger the NoMethodError.
So I think we're mostly agreeing, but I do think you get to write fewer tests overall.
> It obviously can happen, but I just don't see those name collisions ever happening.
It has happened to me (I think in the Rails context less often) but I agree it's not exactly common. It is deadly when it does, though.
Having types not line up at various boundaries (DB/API/reading from a file) is already a pain, but Ruby made it worse by having that bad data pass through many layers until it actually blows up somewhere far removed from the issue. I worked on very real bugs where e.g. a corner case lead to a date being deserialized as a string and then because the last few characters were numeric interpreted as a number so when treated as a date resolved as millis since epoch (or something similarly crazy). It took ~1 of those bugs for me to be convinced that I had no interest in dealing with those kinds of problems, and adding a 'Date' type means it fails in exactly the right place immediately and is a 2 second fix.
I agree with you though - it'd be a silly test to write, so how do you get the correctness/robustness without either types or tests that look like they're effectively validating types?
That's not really accurate - Postgres (for instance) types ARE mapped to Ruby types whenever you read something from the database, just like they are being mapped to Java types or any other language. I guess you mean you can have inadvertent type coercion where a Ruby string is saved into a PG numeric column or something?
Anyway for super sensitive code (let's say payments/prices) you can work with DRY types or Sorbet or any other solution. As a rule it's quite rare that I actually see these problems. Can they occur? Sure. Do they happen so often I wish I was using C++? No. In fact I can't even say it happens more than once a year that I see this type of bug.
Did Java for 13 years. Then moved to Ruby and lost the type system I had leaned so hard on.
After an adjustment period I got into the new groove of just testing all code, and I don't miss my hard typed days at all.
* Static typing catches many kinds of bugs earlier by simply not allowing you to write incorrect code in the first place.
* No matter how good your test suite is, you're still putting the burden on the human to always remember to write tests for corner cases.
* Static typing allows you have to write many fewer tests by making invalid data unrepresentable. You don't have to write millions of unit tests of the type "what if this list is empty" if the function literally can't accept an empty list.
We just don't write these kind of tests on our Rails codebases and we are fine. The world hasn't exploded yet anyway. If some piece of code is super tricky and sensitive then sure maybe then (though I have yet to see such a test case I think), but as a rule? No.
https://sorbet.run/#%23%20typed%3A%20true%0Aextend%20T%3A%3A...
Null is simply the most frequent example of this issue. Getting an integer rather than a string is super common, for example. Or a string instead of a date.
I don't write a million tests. There are much better strategies for testing.
Simple tests that just executes the code will catch the vast majority of type mistakes.
I sometimes get annoyed when I get stuck screwing around with the RBI files. Then I get in the flow and remember how fast Sorbet allows me to move.
I would say that's a big overstatement.