https://careers.velory.com/blog/posts/49020-best-practices-as-code-using-rubocop This website uses cookies to ensure you get the best experience on our website. We and selected partners use cookies or similar technologies to ensure you get the best experience on the Teamtailor website. With respect to cookies that are not considered necessary (as specified in the Cookie Policy), we and our selected partners may use cookies for the following purposes: personalized website experience, develop and improve products and in some cases targeted advertisement that are based on your interests. You can manage or disable cookies by clicking on "Manage cookies" at the bottom of each website page. Cookie preferences Accept all Disable all Select which cookies you accept When you visit this website, it may store or retrieve information on your browser, mostly in the form of cookies. This information might be about you, your preferences or your device and is mostly used to make the website work as you expect it to. The information does not usually directly identify you, but it can give you a more personalized web experience. Because we respect your right to privacy, you can choose not to allow some types of cookies. Below is a list of different categories of cookies that may be set and that you can freely change. However, blocking some types of cookies may impact your experience of the site and the services we are able to offer. [*] Strictly necessary These cookies are required and must be accepted to use this site. Vendors: Teamtailor [ ] Analytics These cookies collect data about how visitors use this website. Vendors: Google [ ] Preferences These cookies allow the website to remember choices you make and provide enhanced, more personal features. [ ] Marketing These cookies are used to deliver adverts more relevant to you and your interests. Accept these cookies Career menu * Start * Teams * Locations * Jobs * To the blog * People Employee Log in Candidate Log in to Connect Homepage velory.com Velory logotype Facebook Twitter LinkedIn * 20 Jan 2022 * Picture of Michael Winton Best practices as code using RuboCop Have you ever received a code review that went something along the lines of: There are a few places that I've commented which don't follow the best practices documented here, let's fix up before proceeding! Engineering teams commonly align on practices that we believe encourage good outcomes in a best-practices.mddocument or similar. These documents are the melting pot in which company history, community standards, new initiatives and hard-earned bug fix lessons meet. So it is only natural for reviews like the one above to occur for changes to be consistent with the best-practices.md. All good, right? Yes, but no. I believe there are a couple of points of friction with this process. First, the process inherently relies on the team's ability to recall and evaluate every practice, in every change, in every code review, in the earnest hope that bad practices don't slip through. Second, the process can dilute the signal to noise ratio of the code review, wherein the worst-case scenario, reviewers and reviewees miss opportunities for discussion on nuances of the change itself. So at Velory, we wondered how we could improve this process. Making it easier to apply our best practices while tightening the feedback loop so that issues are caught long before code review. Imagine the nirvana when you are writing code that would introduce a bad practice and your editor gave the instant feedback, "Hi, avoid Foo. Use Bar instead", that would be delightful. That's what we're after. Towards nirvana We use Rails to develop our applications and RuboCop to lint our Ruby code. RuboCop does a marvellous job of checking and maintaining code style through Cops - rules that identify issues, and optionally automatically fix them too. At Velory, our first step on this journey has been through leveraging RuboCop. By synthesising our best practices into Cops we are essentially able to lint our best practices -- consistently and with a short feedback loop! A reasonable workflow for turning a practice into a Cop has been to: 1. Choose a best practice Not all best practices are equal, but a good place to start are best practices that are grep(1)-able. In this example, we can use: - Avoid `let` and `let!` in specs, prefer four-phase test style Which is grep(1)-able with: grep 'let(.*)' -r spec/**/*.rb 2. Identify examples Often there are a few ways in which a bad practice can be written, so identifying these ways helps towards writing a water-tight Cop. # bad let(:foo) { "foo" } # bad, bad let!(:bar) { "bar" } 3. Write failing specs Using the examples we can now write failing specs that will guide the implementation. require "spec_helper" require "rubocop" require "rubocop/rspec/support" require "rubocop/cop/velory/lets_not" RSpec.describe RuboCop::Cop::Velory::LetsNot do include RuboCop::RSpec::ExpectOffense it "adds offenses for uses of `let`" do expect_offense(<<~RUBY) let(:foo) { "foo" } ^^^^^^^^^ Avoid `let` and `let!`. Instead inline the instance within the `it` block to follow the four-phase test pattern. RUBY end it "adds offenses for uses of `let!`" do expect_offense(<<~RUBY) let!(:bar) { "bar" } ^^^^^^^^^^ Avoid `let` and `let!`. Instead inline the instance within the `it` block to follow the four-phase test pattern. RUBY end def cop @_cop ||= RuboCop::Cop::Velory::LetsNot.new end end This is in my opinion the most important step. Here we design our developer experience, and to make it a delightful one, I recommend using an actionable message. So the developer understands the why of the offense and is equipped with the context to successfully move forwards with a solution. 4. Write the Cop This is the "Draw the rest of the Owl" step, where we need to be familiar with how RuboCop matches code. A tip for writing Cops where the example code is grep(1)-able is to start with the on_send matcher and nodeinstance methods to find matches. This approach takes us pretty far and is all we require for our "Let's Not" best practice. require "rubocop" class RuboCop::Cop::Velory::LetsNot < RuboCop::Cop::Base MSG = "Avoid `let` and `let!`. Instead inline the instance " \ "within the `it` block to follow the four-phase test " \ "pattern." def on_send(node) return unless node.command?(:let) || node.command?(:let!) add_offense(node) end end 5. Run on the project Once the specs are green it's time to run the Cop on the project! bundle exec rubocop Inspecting 1155 files ......................CC...........C.............C.............. 1155 files inspected, 43 offenses detected With RuboCop linting our "Let's Not" best practice we find fourty-three existing offenses in one of our projects. Finding existing offenses to the rule is not an entirely surprising result, c'est la vie. However, on closer inspection, all of these offenses are used within shared_examples. In scenarios like this we have an excellent opportunity to evaluate the best practice as a team, questioning: + is this an exception to the best practice? + is it really a best practice if we have gone against it X times? + should we write out the existing offenses? If the team thinks it's an exception, then we can loop back to step three, refine our examples and update the Cop with the exception in mind. If the team thinks it's no longer a best practice, we can delete it. And if the team thinks that we should write out the offenses we can use rubocop --auto-gen-config and leverage shitlist driven development. Parting Thoughts Encoding your best-practices.md document with RuboCop is not a drop-in replacement, but it can be an excellent tool in your team's toolbox for tightening these feedback loops and ensuring that best practices are held. Writing custom Cops is not always straightforward and some best practices are too subjective to be converted. But that's OK. We can delegate our objective best practices to RuboCop and save our thinking caps for the things that matter. For further reading I recommend RuboCop's custom Cop development guide, EvilMartian's blog post on custom Cops, as well as the open sourced custom Cops from Airbnb, Discourse, GitHub, GitLab and Shopify. Finally, here is our finished "Let's Not" best practice which has been refined to "Let's Not (Outside Of Shared Examples)": require "rubocop" class RuboCop::Cop::Velory::LetsNot < RuboCop::Cop::Base MSG = "Avoid `let` and `let!`. Instead inline the instance within " \ "the `it` block to follow the four-phase test pattern. " \ "Shared examples are an exception to this rule." def_node_matcher :in_shared_example?, <<-PATTERN { (block (send _ #shared_example_context? ...) ...) } PATTERN def on_send(node) return unless let_used?(node) && !in_shared_example_block?(node) add_offense(node) end private def let_used?(node) node.command?(:let) || node.command?(:let!) end def in_shared_example_block?(node) node.each_ancestor(:block).any?(&method(:in_shared_example?)) end def shared_example_context?(element) %w[ it_behaves_like it_should_behave_like include_examples shared_examples ].include?(element.to_s) end end And with that, bliss: bundle exec rubocop Inspecting 1155 files ................................................................ 1155 files inspected, no offenses detected Picture of Michael Winton Michael Winton About Velory Founded in 2014 Coworkers 55 Career site * Start * Teams * Locations * Jobs * To the blog * People * Data & privacy * Manage cookies Locations * Gothenburg * Stockholm Teams * Finance and Legal * Sales & Partners * Product * People & Culture * Operations & Analytics * Brand & Comms velory.com Employee login Candidate Connect login Already working at Velory? Let's recruit together and find your next colleague. [ ] @velory.com * * * * * * * * Career site by Teamtailor