Service Objects in the Wild

A Safari Through Testing with Minitest and the Actor Gem

Posted on July 23, 2023 11 minute read

Testing is a crucial part of software development, and understanding how to effectively use tools like Minitest, mocks, and stubs is essential. This blog post explores these concepts in the context of Ruby, specifically focusing on the Actor gem for creating service objects.

Introduction

As a mentor, I’ve frequently come across questions from developers transitioning from RSpec or other languages to Minitest. The shift can be challenging, especially when it comes to understanding the nuances of testing, mocking, and stubbing in a new environment. These conversations with my mentees have not only highlighted the common struggles but also the need for a comprehensive guide that bridges the gap.

In this blog post, I aim to provide that bridge, focusing specifically on testing with Minitest and the Actor gem. Whether you’re an RSpec veteran exploring Minitest or a developer coming from another language, this post is designed to ease your transition and enhance your testing skills in Ruby.

Minitest, a popular testing framework in the Ruby ecosystem, offers a compelling alternative to RSpec, you can learn more about it in the official Minitest documentation. Known for its simplicity and minimalism, Minitest provides a straightforward testing experience without unnecessary complexity. Its speed, coupled with out-of-the-box parallelism, allows for quick development cycles, especially under Ruby on Rails. Minitest’s more “Ruby-like” approach, with less reliance on DSLs, makes the code transparent and easy to understand. Being the choice of both Ruby and Rails, it aligns well with foundational technologies. If you value a lightweight, fast, and clear testing framework, giving Minitest a go might be the perfect decision.

We’ll be using some fun animal-themed examples involving penguins and crocodiles, where we leverage the Actor gem to write elegant service objects. So buckle up, and let’s dive into the world of testing with Minitest!

Understanding Mocks and Stubs

In the world of testing, mocks and stubs are two common techniques used to isolate the code under test from its dependencies. Though the terms are often used interchangeably, they serve different purposes. Let’s explore these concepts, drawing from the definitive resources by Martin Fowler and his article Mocks Aren’t Stubs.

What Are Stubs?

Stubs provide canned answers to calls made during the test. They are used to control the indirect inputs of the system under test. Stubs don’t usually respond to anything outside what’s programmed in for the test.

Example: Testing a PenguinFeeder class with a Stub

require "minitest/autorun"

class PenguinFeeder
  def feed(penguin)
    penguin.eat(:fish)
  end
end

class PenguinFeederTest < Minitest::Test
  def test_feed_penguin
    penguin = Minitest::Mock.new
    penguin.expect :eat, true, [:fish]

    feeder = PenguinFeeder.new
    assert feeder.feed(penguin)
  end
end

run it with ruby penguin_feeder.rb:

Run options: --seed 19621

# Running:

.

Finished in 0.001013s, 987.6212 runs/s, 987.6212 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

What Are Mocks?

Mocks, on the other hand, are objects pre-programmed with expectations. They are used to verify the indirect outputs of the system under test, ensuring that it interacts with its collaborators in expected ways.

Example: Testing a CrocodileTrainer class with a Mock

require "minitest/autorun"

class CrocodileTrainer
  def train(crocodile)
    crocodile.perform(:roll_over)
  end
end

class CrocodileTrainerTest < Minitest::Test
  def test_train_crocodile
    crocodile = Minitest::Mock.new
    crocodile.expect :perform, true, [:roll_over]

    trainer = CrocodileTrainer.new
    assert trainer.train(crocodile)
    crocodile.verify
  end
end

run it with ruby crocodile_trainer.rb:

Run options: --seed 1695

# Running:

.

Finished in 0.000683s, 1464.3561 runs/s, 1464.3561 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

When to Use Stubs vs Mocks?

Use Stubs When:

  • You want to control the behaviour of a dependency.
  • You’re not concerned with how the dependency is used, only with its output.

Use Mocks When:

  • You want to ensure that a dependency is used correctly by the system under test.
  • You’re concerned with the interaction between the system and its dependencies.

Understanding the differences between mocks and stubs, and knowing when to use each, is essential for writing effective tests. Stubs help you control the behaviour of dependencies, while mocks allow you to verify interactions. By leveraging both techniques, you can write more robust and maintainable tests.

Mocking and Stubbing in Minitest

Mocking and stubbing are techniques used in testing to isolate the code under test from its dependencies. Here’s a quick overview:

  • Mocking: Replacing a real object with a fake one that expects certain calls and returns predefined responses.
  • Stubbing: Replacing a method on a real object with a fake method that returns a predefined response.

Minitest’s Approach to Mocking and Stubbing

Minitest provides built-in support for both mocking and stubbing. Let’s explore how we can use these features through some animal-themed examples.

Mocking in Minitest: A Penguin example

Example: Testing a PenguinFeeder Class

Suppose we have a class PenguinFeeder that feeds penguins. We want to test this class without actually feeding real penguins (they might get too full!). Here’s how we can do it:

require 'minitest/autorun'

class Penguin
  def eat(food)
    food == :fish
  end
end

class PenguinFeeder
  def feed(penguin)
    penguin.eat(:fish)
  end
end

class PenguinFeederTest < Minitest::Test
  def test_feed_penguin
    penguin = Minitest::Mock.new
    penguin.expect :eat, true, [:fish]

    feeder = PenguinFeeder.new
    assert feeder.feed(penguin)
    penguin.verify
  end
end

We created a mock penguin object and expect it to receive the eat method with the argument :fish.

When we run the test with ruby penguin_feeder_mock.rb, we get the following output:

Run options: --seed 42862

# Running:

.

Finished in 0.000783s, 1277.5014 runs/s, 1277.5014 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Stubbing in Minitest: A Crocodile example

Stubbing is another powerful technique in testing. Let’s see how we can use stubbing in Minitest with a crocodile-themed example. Example: Testing a CrocodileTrainer class

Imagine we have a class CrocodileTrainer that trains crocodiles to perform tricks. We want to test this class without actually training real crocodiles (they might not cooperate!). Here’s our class:

require "minitest/autorun"

class Crocodile
  def perform(action)
    "🐊🙄 performing #{action}"
  end
end

class CrocodileTrainer
  def train(crocodile)
    crocodile.perform(:roll_over)
  end
end

class CrocodileTrainerTest < Minitest::Test
  def test_train_crocodile
    crocodile = Crocodile.new
    perform_action = :roll_over
    stubbed_response = "🐊😃 performing #{perform_action}"

    # Stubbing the perform method to return a specific response
    crocodile.stub :perform, stubbed_response do
      trainer = CrocodileTrainer.new

      # Asserting that the stubbed method is called with the correct argument
      assert_equal stubbed_response, trainer.train(crocodile)

      # Asserting that the original method is not called
      refute_equal "🐊🙄 performing #{perform_action}", trainer.train(crocodile)
    end
  end
end

We can stub the perform method on a real crocodile object to return a predefined response.

When we run the test with ruby crocodile_trainer_stub.rb, we get the following output:

Run options: --seed 24139

# Running:

.

Finished in 0.000652s, 1533.6882 runs/s, 3067.3765 assertions/s.

1 runs, 2 assertions, 0 failures, 0 errors, 0 skips

Great! We’ve trained our virtual crocodile without any real-life risks.

The Actor Gem and Service/Business Logic Layer

The Actor gem is one of the options available for creating service objects or command objects in Ruby. It offers a robust and simple way to organize business logic, providing a clear structure for handling inputs, outputs, errors and other service layer facilities, the Actor gem can be a valuable tool for developers looking to maintain clean and testable code.

Why Use Actor for Service or Command Objects?

Service objects encapsulate a specific business operation, while command objects represent a command to be executed. Using the Actor gem for these purposes offers several benefits:

  • Readability: Clear separation of inputs, outputs, and the operation itself.
  • Testability: Easier to test individual components.
  • Reusability: Encourages modular design, making it easier to reuse code.

Importance of a Service/Business Logic Layer

Managing business logic as an architectural component achieves greater modularity. By keeping it separated from the usual Rails application concepts, we can:

  • Reduce Complexity: Isolate complex business rules from controllers and models.
  • Enhance Maintainability: Make changes to business logic without affecting other parts of the application.
  • Improve Testability: Write more focused and efficient tests.

Example: PenguinAdoptionService

Let’s create a service object using the Actor gem that handles the adoption of penguins. Here’s our class:

require "service_actor"

class Penguin
  attr_accessor :available_for_adoption

  def initialize(available_for_adoption)
    @available_for_adoption = available_for_adoption
  end

  def adopt(adopter)
    available_for_adoption = false
    adopter.receive('🐧😍')
  end

  def available_for_adoption?
    available_for_adoption == true
  end
end

class Adopter
  attr_reader :can_adopt

  def initialize(can_adopt)
    @can_adopt = can_adopt
  end

  def can_adopt?
    @can_adopt == true
  end

  def receive(message)
    puts message
  end
end

class PenguinAdoptionService < Actor
  input :penguin
  input :adopter

  output :adoption_status

  def call
    if adopter.can_adopt? && penguin.available_for_adoption?
      penguin.adopt(adopter)
      self.adoption_status = 'Adoption Successful!'
    else
      self.adoption_status = 'Adoption Failed!'
    end
  end
end


We can now use this service object to handle the adoption process in a clean and structured way.

penguin = Penguin.new(true)
adopter = Adopter.new(true)
result = PenguinAdoptionService.call(penguin: penguin, adopter: adopter)
puts result.adoption_status

and we get the result:

🐧😍
Adoption Successful!

Testing with the Actor Gem

The Actor gem provides a powerful way to manage business logic as an architectural component, keeping it separate from the usual Rails application concepts. This separation achieves greater modularity and makes testing more straightforward. In this section, we’ll explore how to test actors using both mocks and stubs.

Testing with Stubs

Stubs can be used to control the behaviour of dependencies within an actor. Here’s an example of how you might test a PenguinDance actor that depends on a MusicPlayer actor.

Example: PenguinDance Actor with MusicPlayer Stub

require 'minitest/autorun'
require 'service_actor'

class MusicPlayer
  def self.play(song)
    "Playing #{song}"
  end
end

class PenguinDance < Actor
  input :music_player, default: MusicPlayer

  def call
    music_player.play(:happy_feet)
  end
end

class PenguinDanceTest < Minitest::Test
  def test_dance_to_music
    MusicPlayer.stub :play, true do
      result = PenguinDance.call
      assert result.success?
    end
  end
end

Testing with Mocks

Mocks can be used to ensure that an actor interacts with its dependencies in the expected way. Here’s an example of how you might test a CrocodileSwim actor that depends on a WaterPool actor. In this example we use a Mock class, that mimics a class built with Actor gem.

Example: CrocodileSwim Actor with WaterPool Mock

require 'minitest/autorun'
require 'service_actor'

class WaterPool < Actor
  input :type

  output :capacity

  def call
    puts "Filling with #{type}"
    self.capacity = :full
  end
end

class CrocodileSwim < Actor
  input :water_pool, default: WaterPool

  output :completed_pools

  def call
    outcome = water_pool.call(type: :fresh_water)

    self.completed_pools = outcome.capacity == :full ? 23 : 0
  end
end

class MockService
  def initialize(expected_arguments, return_value = nil)
    @expected_arguments = expected_arguments
    @return_value = return_value
  end

  def call(args)
    raise 'Unexpected arguments' unless arguments_match?(@expected_arguments, args)

    ServiceActor::Result.new(@return_value)
  end

  private

  def arguments_match?(expected, actual)
    expected == actual
  end
end

class CrocodileSwimMockServiceTest < Minitest::Test
  def test_swim_in_pool
    mock_service = MockService.new({ type: :fresh_water }, { capacity: :full })

    outcome = CrocodileSwim.call(water_pool: mock_service)
    assert_equal 23, outcome.completed_pools
  end
end

Since Actor gem allows dependency injection this is perfectly possible, so we’re swapping the real implementation with a dummy service, that answers to the same methods. In Ruby If it walks like a duck and it quacks like a duck, then it must be a duck. is a mantra. For more information check Wikipedia page for Duck Typing

But this is not the only method to use Mocks, with minitest, as minitest has its own mocking facilities. Actually we can swap our test implementation with:

class CrocodileSwimMinitestMock < Minitest::Test
  def test_swim_in_pool
    mock_service = Minitest::Mock.new
    mock_service.expect :call, ServiceActor::Result.to_result({ capacity: :full }), type: :fresh_water
    # due internal actor checks, if absent an error is raised:
    # NoMethodError: unmocked method :nil?, expected one of [:call]
    mock_service.expect :nil?, false

    outcome = CrocodileSwim.call(water_pool: mock_service)
    assert_equal 23, outcome.completed_pools
    # Verify that all methods were called as defined by expected. Will raise an exception otherwise
    mock_service.verify
  end
end

This method has the advantage of ensuring all the method calls outlined in by the expect method are indeed called, adding an extra layer of checks, compared with the MockService defined earlier. But it’s Ruby and it’s using dependency injection, so at the end of the day, it’s a matter of choosing the most appropriate implementation.

Choosing the Right Approach

Both mocks and stubs have their place in testing, and choosing the right approach depends on the specific needs of your test.

Use Stubs When:

  • You want to isolate the system under test from its dependencies.
  • You need to control the behaviour of dependencies without verifying interactions.

Use Mocks When:

  • You want to ensure that the system under test interacts with its dependencies in a specific way.
  • You need to verify both the behaviour and the interactions of dependencies.

Conclusion

Testing is a vital part of software development, and understanding how to use mocks and stubs effectively can lead to more robust and maintainable tests. By leveraging the Actor gem and the principles outlined in this post, developers can write clear and concise tests that ensure their code behaves as expected.

Whether you’re new to testing or an experienced developer, we hope this post has provided valuable insights into the world of mocks, stubs, and testing with the Actor gem. Happy coding!

If you want to ask some question or share your thoughts please contact me from the links on the left.