7 ms·
Speed Up Your Rails Specs
- xentronium 13y agoRE: Developing Decoupled Code Extra complexity cost is insane and probably not worth it. Basically you move all your crazy stubbing and mocking out of test code and into application code. Do not do this unless you really, really need this kind of dependency injection.
- mattgreenrocks 13y agoBack in the day, we laughed at people who stuffed all their code into button click handlers. Now, we mock people who don't. My, how different the web is!
- xentronium 13y agoThat's a straw man. I see how you made it up for a joke, but it's still confusing. "Don't overengineer" is not the same as "stuff all your code into button click handlers".
- al2o3cr 13y ago"expect(widget_factory).to have_received(:create).with({name: 'sprocket'})" Even better: expect("my_service.rb", "line X").to be("widget_factory.create") Or yet better, simply: ; # realize that testing the exact implementation is bullshit
- joevandyk 13y agoMaybe if ruby didn't take so long to load rails code, this sort of complexity wouldn't be necessary...
- jggkffkfjfjfjfj 13y agospeed up by 10? Move to python. Speed up by 100? Move to Go. Speed up by 1000? Move to C++.
- sanderjd 13y agoNote: Ruby and Python have pretty nearly identical performance for most workloads these days. Other than that, it's an illustrative point - if what you really care about is execution speed, you should pick a language that focuses on it more. It's interesting that increased developer productivity is the classic trade-off for decreased execution speed, but that all these "my tests are slow" issues are basically decreased execution speed impacting developer productivity.
- j-kidd 13y agoThis is not strictly about Ruby vs Python. Having used Pyramid (+SQLAlchemy) and Rails, I think the former is plenty fast enough such that no user cares about silly optimization, while the latter is the opposite. Loading the Rails environment is just too slow, thus you need a preloader such as Zeus or Spring. And then you need something like Guard to make unit testing semi-bearable. But running the whole test suite would still be too slow, so you need parallel_tests to spread the tests across multiple cores (and multiple databases). And finally you drink the PORO kool-aid and start decoupling your codebase from Rails stuffs, and end up debating with DHH in HN.
- sanderjd 13y agoGreat comment, I agree wholeheartedly, it's a mess. I do think using PORO's and not arguing with DHH on HN is one reasonable option that you missed though.
- mcmire 13y agoThe problem with dependency injection in this manner is that your test code is using a fake class that's injected while your production code is using a real class. This is fine if all you want to do is unit test your classes. That's beside the point though -- I would posit if you're bolting dependency injection onto your interfaces just to make your tests faster, you're not doing it right. Instead, bring that dependency injection into the forefront: to the constructor. Yes, this means to make an object, you'll have to make another object first, both in your production and test code. But now your interface acts the same way in both places. If this sounds very Java-like, well... maybe Java got it right. (There was an article on the Twitter blog in the early days about this but I can't remember where it is now...) Of course, this is going to introduce complexity and now you have to decide whether that complexity is worth it.
- jdminhbg 13y agoI like that the "tightly coupled" service class is 3 extremely simple LOC that should probably just be in a controller anyway, which is "fixed" by creating a class with 3X as many methods and at least twice as much logic to follow, plus a spec twice as long as the original class that tests nothing other than the fact that when you call a method with a set of arguments, that method receives that set of arguments.
- ryanbrunner 13y agoI have a massive, and probably not entirely proportionate, negative reaction to constructor injection. It's in my mind the ultimate example of making your code more complex in service of your tests. An approach that I've used recently (which is similar enough to parameter-style injection, is to use `class_attribute` at the top of a class to call out dependencies and explicitly provide a default value - so that consumers of your class don't need to know what exactly a WidgetFactory is supposed to be: class Foo class_attribute :some_service self.some_service = SomeService.new end A nice advantage of this approach is that you can provide a different implementation of some_service on an individual instance of a class, which helps restore your code to a sane state when you're done with an individual test, like so: describe Foo subject(:foo) { Foo.new } before { foo.some_service = double(SomeService).as_null_object } end On top of that, though, I think calling out dependencies really only makes sense when that dependency involves something external to the codebase itself, or is otherwise secondary to the main purpose of the class. So stub out a repository, or a logger to your hearts content - but there's little utility in stubbing out Widget in a WidgetBuilder class IMO.