Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to Write Better Code with Mutation Testing

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

How to Write Better Code with Mutation Testing

Presentation for Railsconf and Fog City Ruby based on https://blog.blockscore.com/how-to-write-better-code-using-mutation-testing/.

Mutation testing can help you:

* Improve your test coverage
* Think about edge cases and catch more bugs before they break in production
* Learn more about Ruby, the libraries you depend on, and your own codebase

Avatar for John Backus

John Backus

January 10, 2017
Tweet

Other Decks in Programming

Transcript

  1. 1 class Gluttons 2 def initialize(twitter_client) 3 @twitter = twitter_client

    4 end 5 6 def recent 7 query = @twitter.search('"I really enjoy #pizza"') 8 9 query.first(2).map { |tweet| "@#{tweet.author}" } 10 end 11 end 1 RSpec.describe Gluttons do 2 it 'lists the two most recent gluttonous tweeters' do 3 tweets = [double(author: 'John'), double(author: 'Jane')] 4 gluttons = Gluttons.new(double(search: tweets)) 5 6 expect(gluttons.recent).to eql(%w[@John @Jane]) 7 end 8 end
  2. 1 class Gluttons 2 def recent 3 - query =

    @twitter.search('"I really enjoy #pizza"') 4 + query = @twitter.search('"I really enjoy #hotdogs"') 1 example, 0 failures
  3. 1 class Gluttons 2 def recent 3 - query =

    @twitter.search('"I really enjoy #pizza"') 4 + query = @twitter.search('') 1 example, 0 failures
  4. 1 class Gluttons 2 def recent 3 - query =

    @twitter.search('"I really enjoy #pizza"') 4 + query = @twitter.search 1 example, 0 failures
  5. 1 class Gluttons 2 def recent 3 - query.first(2).map {

    |tweet| "@#{tweet.author}" } 4 + query.first(1).map { |tweet| "@#{tweet.author}" } Failure expected: ["@John", "@Jane"] got: ["@John"]
  6. 1 - query.first(2).map { |tweet| "@#{tweet.author}" } 2 + query.first(3).map

    { |tweet| "@#{tweet.author}" } 3 end 1 example, 0 failures
  7. 1 def recent 2 - query = @twitter.search('"I really enjoy

    #pizza"') 3 + query = @twitter.search
  8. 1 def recent 2 - query = @twitter.search('"I really enjoy

    #pizza"') 3 + query = @twitter.search(nil)
  9. 1 it 'lists the two most recent gluttonous tweeters' do

    2 tweets = [ 3 double(author: 'John'), 4 double(author: 'Jane'), 5 double(author: 'Devon') 6 ] 7 8 client = double('Client') 9 gluttons = Gluttons.new(client) 10 11 allow(client) 12 .to receive(:search) 13 .with('"I really enjoy #pizza"') 14 .and_return(tweets) 15 16 expect(gluttons.recent).to eql(%w[@John @Jane]) 17 end
  10. 1 it 'returns a user when given a valid id'

    do 2 expect(get(:show, id: 1)).to eq(id: 1, name: 'John') 3 end 4 5 it 'renders JSON error when given an invalid id' do 6 expect(get(:show, id: 0)) 7 .to eq(error: "Could not find User with 'id'=0") 8 end 1 class UsersController < ApplicationController 2 def show 3 render json: User.find(params[:id].to_i) 4 rescue User::RecordNotFound => error 5 render json: { error: error.to_s } 6 end 7 end
  11. 1 def show 2 - render json: User.find(params[:id].to_i) 3 +

    render json: User.find(Integer(params[:id])) 4 rescue User::RecordNotFound => error 5 render json: { error: error.to_s } 6 end
  12. 1 def show 2 - render json: User.find(params[:id].to_i) 3 +

    render json: User.find(params.fetch(:id).to_i) 4 rescue User::RecordNotFound => error 5 render json: { error: error.to_s } 6 end
  13. 1 def show 2 - render json: User.find(params[:id].to_i) 3 +

    render json: User.find(Integer(params.fetch(:id))) 4 rescue User::RecordNotFound => error 5 render json: { error: error.to_s } 6 end
  14. 1 class UsersController < ApplicationController 2 def created_after 3 after

    = Date.parse(params[:after]) 4 render json: User.recent(after) 5 end 6 end
  15. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end
  16. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end
  17. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end “2017-05-01" "H29.05.01" "Tue May 01 00:00:00 2017" "Tue, 01 May 2017 00:00:00 +0000" "Tue, 01 May 2017 00:00:00 GMT" "May" "I may be complete garbage" “2017-05-01" Date.parse Date.iso8601
  18. 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3

    + username =~ /\A(John|Alain).+$/ 4 end 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3 + username =~ /^(John|Alain).+\z/ 4 end
  19. 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3

    + username =~ /^(Alain).+$/ 4 end 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3 + username =~ /^(John).+$/ 4 end
  20. 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3

    + username.match?(/^(John|Alain).+$/) 4 end
  21. 1 usernames.select do |username| 2 - username =~ /^(John|Alain).+$/ 3

    + username.match?(/\A(?:John|Alain).+\z/) 4 end
  22. 1 def stars_for(repo) 2 url = "https://api.github.com/repos/#{repo}" 3 data =

    HTTParty.get(url).to_h 4 5 data['stargazers_count'] 6 end
  23. 1 def stars_for(repo) 2 url = "https://api.github.com/repos/#{repo}" 3 - data

    = HTTParty.get(url).to_h 4 + data = HTTParty.get(url) 5 6 data['stargazers_count'] 7 end
  24. 1 class UsersController < ApplicationController 2 def created_after 3 after

    = Date.parse(params[:after]) 4 render json: User.recent(after) 5 end 6 end
  25. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end
  26. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end “2017-05-01" "H29.05.01" "Tue May 01 00:00:00 2017" "Tue, 01 May 2017 00:00:00 +0000" "Tue, 01 May 2017 00:00:00 GMT" "May" "I may be complete garbage" “2017-05-01" Date.parse Date.iso8601
  27. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end What was the author’s intent?
  28. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end How is this code used today?
  29. 1 def created_after 2 - after = Date.parse(params[:after]) 3 +

    after = Date.iso8601(params[:after]) 4 render json: User.recent(after) 5 end Should I update the code or the tests?
  30. 1 def can_buy_alcohol?(age) 2 - age >= 21 3 +

    age >= 22 4 end 1 def can_buy_alcohol?(age) 2 - age >= 21 3 + age >= 20 4 end
  31. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end
  32. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :guest 1
  33. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :guest 1 :muted 2
  34. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :user 3 when :guest 1 :muted 2
  35. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :user 3 when :guest 1 :muted 2 !post.locked? 4
  36. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :user 3 when :guest 1 :muted 2 && 5 !post.locked? 4
  37. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :moderator 6 when :user 3 when :guest 1 :muted 2 && 5 !post.locked? 4
  38. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :moderator 6 when :user 3 when :guest 1 :muted 2 && 5 !post.locked? 4 then !post.author.admin? 7
  39. 1 def can_edit_post?(editor, post) 2 case editor.role 3 when :guest,

    :muted then false 4 when :user 5 editor == post.author && !post.locked? 6 when :moderator then !post.author.admin? 7 when :admin then true 8 end 9 end when :admin 8 when :moderator 6 when :user 3 when :guest 1 :muted 2 && 5 !post.locked? 4 then !post.author.admin? 7
  40. guest muted user moderator admin guest muted editor different user

    moderator admin Editor’s Role Post Author’s Role
  41. 1 def mailing_list(users) 2 users.map do |user| 3 next unless

    user.email && !user.unsubscribed? 4 5 user.email 6 end.compact 7 end
  42. 1 it 'filters out users without emails' do 2 users

    = [good_user, user_without_email] 3 expect(mailing_list(users)).to eql([good_user.email]) 4 end 5 6 7 it 'filters out users unsubscribed users' do 8 users = [good_user, unsubscribed_user] 9 expect(mailing_list(users)).to eql([good_user.email]) 10 end
  43. 1 def mailing_list(users) 2 users.map do |user| 3 - next

    unless user.email && !user.unsubscribed? 4 + break unless user.email && !user.unsubscribed? 5 6 user.email 7 end.compact 8 end
  44. 1 it 'filters out users without emails' do 2 -

    users = [good_user, user_without_email] 3 + users = [user_without_email, good_user] 4 expect(mailing_list(users)).to eql(good_user.email) 5 end 6 7 8 it 'filters out users unsubscribed users' do 9 - users = [good_user, unsubscribed_user] 10 + users = [unsubscribed_user, good_user] 11 expect(mailing_list(users)).to eql(good_user.email) 12 end
  45. 1 class User < ActiveRecord::Base 2 def find_by_name(name) 3 -

    find_by(name: name) 4 + super 5 end 6 end
  46. 1 class PostsController < ApplicationController 2 private 3 4 def

    authorized?(user = current_user) 5 # ... 6 end 7 end
  47. 1 class PostsController < ApplicationController 2 def edit 3 return

    unauthorized unless authorized?(current_user) 4 end 5 6 def admin_edit 7 return unauthorized unless authorized?(current_user) 8 end 9 10 private 11 12 def authorized?(user = current_user) 13 # ... 14 end 15 end
  48. 1 class PostsController < ApplicationController 2 - def authorized?(user =

    current_user) 3 + def authorized?(user) 4 # ... 5 end 6 end
  49. 1 class PostsController < ApplicationController 2 def edit 3 return

    unauthorized unless authorized? 4 end 5 6 def admin_edit 7 return unauthorized unless authorized? 8 end 9 10 private 11 12 def authorized?(user = current_user) 13 # ... 14 end 15 end
  50. 1 module MyApp 2 class User 3 def posted? 4

    ::MyApp::Post.exists?(user_id: id) 5 end 6 end 7 end
  51. 1 module MyApp 2 class User 3 def posted? 4

    - ::MyApp::Post.exists?(user_id: id) 5 + Post.exists?(user_id: id) 6 end 7 end 8 end
  52. 1 class PostsController < ApplicationController 2 def show 3 render

    json: Post.find(params[:id]), status: :ok 4 end 5 end
  53. 1 class PostsController < ApplicationController 2 def show 3 -

    render json: Post.find(params[:id]), status: :ok 4 + render json: Post.find(params[:id]) 5 end 6 end
  54. 1 class UserDecorator 2 attr_reader :user 3 4 def initialize(user)

    5 @user = user 6 end 7 8 def greeting 9 "Welcome, #{@user.name}!" 10 end 11 end
  55. 1 class UserDecorator 2 attr_reader :user 3 4 def initialize(user)

    5 @user = user 6 end 7 8 def greeting 9 - "Welcome, #{@user.name}!" 10 + "Welcome, #{user.name}!" 11 end 12 end
  56. 1 def prune_inactive_images 2 images = Image.where("last_viewed_at > ?", 2.years.ago)

    3 4 images.map do |image| 5 puts "Deleting image #{image.name}" 6 end 7 8 images.destroy_all 9 10 images.count 11 end
  57. 1 def prune_inactive_images 2 images = Image.where("last_viewed_at > ?", 2.years.ago)

    3 4 - images.map do |image| 5 + images.each do |image| 6 puts "Deleting image #{image.name}" 7 end 8 9 images.destroy_all 10 11 images.count 12 end
  58. 1 require 'logger' 2 3 logger = Logger.new($stdout) 4 5

    logger.formatter = 6 Proc.new do |severity, datetime, name, msg| 7 "[#{severity}] #{datetime} #{name} -- #{msg}\n" 8 end
  59. 1 require 'logger' 2 3 logger = Logger.new($stdout) 4 5

    logger.formatter = 6 - Proc.new do |severity, datetime, name, msg| 7 + lambda do |severity, datetime, name, msg| 8 "[#{severity}] #{datetime} #{name} -- #{msg}\n" 9 end
  60. formatter = proc { |a, b| [a, b].inspect } formatter.call()

    # => "[nil, nil]" formatter.call(1) # => "[1, nil]" formatter.call(1, 2) # => "[1, 2]" formatter.call(1, 2, 3) # => "[1, 2]"
  61. formatter = proc { |a, b| [a, b].inspect } formatter.call()

    # => "[nil, nil]" formatter.call(1) # => "[1, nil]" formatter.call(1, 2) # => "[1, 2]" formatter.call(1, 2, 3) # => "[1, 2]" formatter.call([]) # => "[nil, nil]" formatter.call([1]) # => "[1, nil]" formatter.call([1, 2]) # => "[1, 2]" formatter.call([1, 2, 3]) # => "[1, 2]"
  62. formatter = lambda { |a, b| [a, b].inspect } formatter.call()

    # => ArgumentError formatter.call(1) # => ArgumentError formatter.call(1, 2) # => "[1, 2]" formatter.call(1, 2, 3) # => ArgumentError formatter.call([]) # => ArgumentError formatter.call([1]) # => ArgumentError formatter.call([1, 2]) # => ArgumentError formatter.call([1, 2, 3]) # => ArgumentError
  63. 1 module YourApp 2 class User < ActiveRecord::Base 3 #

    Dozens of includes, scopes, class methods 4 5 def validate_email 6 # Simple method you're fixing 7 end 8 end 9 end
  64. 1 RSpec.describe YourApp::User do 2 # 100s of tests and

    setup unrelated to your task 3 4 describe '#validate_email' do 5 # Half dozen tests you are focusing on 6 end 7 end