Inicio / Ruby / Ruby: Lenguaje Elegante y Expresivo / Testing con RSpec y Minitest

Testing con RSpec y Minitest

Minitest, RSpec (describe/it), matchers, let, hooks, mocks, stubs y shared examples.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Testing con RSpec y Minitest

Ruby tiene una cultura de testing muy fuerte. Los dos frameworks principales son Minitest (incluido en Ruby) y RSpec.


Minitest (incluido en Ruby)

# test_calculadora.rb
require 'minitest/autorun'

class Calculadora
  def sumar(a, b) = a + b
  def dividir(a, b)
    raise ArgumentError, "No se puede dividir por cero" if b == 0
    a.to_f / b
  end
end

class TestCalculadora < Minitest::Test
  def setup
    @calc = Calculadora.new
  end

  def test_sumar
    assert_equal 5, @calc.sumar(2, 3)
  end

  def test_sumar_negativos
    assert_equal -1, @calc.sumar(2, -3)
  end

  def test_dividir
    assert_in_delta 3.33, @calc.dividir(10, 3), 0.01
  end

  def test_dividir_por_cero
    assert_raises(ArgumentError) do
      @calc.dividir(10, 0)
    end
  end
end
ruby test_calculadora.rb

RSpec — Setup

gem install rspec
rspec --init
.rspec
spec/
├── spec_helper.rb
└── calculadora_spec.rb
# .rspec
--format documentation
--color
--require spec_helper

RSpec — Sintaxis describe/it

# spec/calculadora_spec.rb
require_relative '../lib/calculadora'

RSpec.describe Calculadora do
  subject(:calc) { described_class.new }

  describe '#sumar' do
    it 'suma dos números positivos' do
      expect(calc.sumar(2, 3)).to eq(5)
    end

    it 'suma números negativos' do
      expect(calc.sumar(-2, -3)).to eq(-5)
    end

    it 'suma con cero' do
      expect(calc.sumar(5, 0)).to eq(5)
    end
  end

  describe '#dividir' do
    it 'divide dos números' do
      expect(calc.dividir(10, 2)).to eq(5.0)
    end

    it 'lanza error al dividir por cero' do
      expect { calc.dividir(10, 0) }.to raise_error(ArgumentError, /cero/)
    end
  end
end
rspec
rspec spec/calculadora_spec.rb
rspec spec/calculadora_spec.rb:10   # línea específica

Matchers de RSpec

# Igualdad
expect(5).to eq(5)           # ==
expect(5).to eql(5)          # eql?
expect(obj).to equal(obj)    # same object (equal?)
expect(5).to be(5)           # equal?

# Comparación
expect(10).to be > 5
expect(10).to be >= 10
expect(5).to be < 10
expect(5).to be_between(1, 10)

# Truthiness
expect(true).to be_truthy
expect(nil).to be_falsy
expect(nil).to be_nil

# Tipos
expect("hola").to be_a(String)
expect(42).to be_an(Integer)
expect([]).to be_an(Array)

# Strings
expect("Hola Mundo").to include("Mundo")
expect("Hola").to start_with("Ho")
expect("Hola").to end_with("la")
expect("Hola").to match(/\AH\w+\z/)

# Arrays
expect([1, 2, 3]).to include(2)
expect([1, 2, 3]).to contain_exactly(3, 1, 2)  # cualquier orden
expect([1, 2, 3]).to match_array([3, 2, 1])
expect([]).to be_empty
expect([1, 2, 3]).to have_attributes(length: 3)
expect([1, 2, 3]).to all(be_an(Integer))

# Hashes
expect({ a: 1 }).to include(a: 1)
expect({ a: 1, b: 2 }).to include(:a, :b)

# Cambios
expect { array.push(1) }.to change(array, :length).by(1)
expect { x += 5 }.to change { x }.from(0).to(5)

# Excepciones
expect { raise "Error" }.to raise_error(RuntimeError)
expect { raise "Error" }.to raise_error("Error")
expect { raise "Error" }.to raise_error(/Err/)

# Output
expect { puts "hola" }.to output("hola\n").to_stdout

# Compuestos
expect(10).to be > 5 .and be < 20
expect("hola").to start_with("ho").and end_with("la")

Contextos y let

RSpec.describe User do
  # let — evaluación lazy (se ejecuta al usar la variable)
  let(:user) { User.new(nombre: "Ana", rol: :user) }
  let(:admin) { User.new(nombre: "Bob", rol: :admin) }

  # let! — evaluación eager (se ejecuta siempre)
  let!(:timestamp) { Time.now }

  describe '#admin?' do
    context 'cuando el usuario es admin' do
      subject { admin }

      it { is_expected.to be_admin }
    end

    context 'cuando el usuario no es admin' do
      subject { user }

      it { is_expected.not_to be_admin }
    end
  end

  describe '#nombre_completo' do
    context 'con apellido' do
      let(:user) { User.new(nombre: "Ana", apellido: "García") }

      it 'retorna nombre y apellido' do
        expect(user.nombre_completo).to eq("Ana García")
      end
    end

    context 'sin apellido' do
      it 'retorna solo el nombre' do
        expect(user.nombre_completo).to eq("Ana")
      end
    end
  end
end

Hooks (before/after)

RSpec.describe Database do
  before(:all) do
    # Una vez antes de TODOS los tests del describe
    @connection = Database.connect
  end

  before(:each) do
    # Antes de CADA test
    @connection.begin_transaction
  end

  after(:each) do
    # Después de CADA test
    @connection.rollback
  end

  after(:all) do
    # Una vez después de TODOS los tests
    @connection.close
  end
end

Mocks y Stubs

RSpec.describe OrderService do
  describe '#create_order' do
    let(:payment_gateway) { instance_double(PaymentGateway) }
    let(:mailer) { instance_double(OrderMailer) }
    let(:service) { OrderService.new(payment_gateway, mailer) }

    it 'procesa el pago y envía email' do
      # Stub — define retorno
      allow(payment_gateway).to receive(:charge)
        .with(100)
        .and_return({ success: true, id: "pay_123" })

      allow(mailer).to receive(:send_confirmation)

      # Act
      result = service.create_order(amount: 100)

      # Mock — verifica que se llamó
      expect(payment_gateway).to have_received(:charge).with(100)
      expect(mailer).to have_received(:send_confirmation).once
      expect(result).to be_success
    end

    it 'no envía email si el pago falla' do
      allow(payment_gateway).to receive(:charge)
        .and_return({ success: false })

      service.create_order(amount: 100)

      expect(mailer).not_to have_received(:send_confirmation)
    end
  end
end

Shared examples

RSpec.shared_examples "colección" do
  it 'empieza vacía' do
    expect(subject).to be_empty
  end

  it 'puede añadir elementos' do
    subject.add("item")
    expect(subject).not_to be_empty
  end

  it 'puede contar elementos' do
    3.times { |i| subject.add("item_#{i}") }
    expect(subject.count).to eq(3)
  end
end

RSpec.describe Stack do
  it_behaves_like "colección"
end

RSpec.describe Queue do
  it_behaves_like "colección"
end

Resumen

Concepto Minitest RSpec
Assertion/Expect assert_equal expect().to eq()
Setup def setup before, let
Test method def test_xxx it 'xxx'
Grouping Class describe, context
Mocks Minitest::Mock instance_double
Run ruby test_file.rb rspec
🔒

Ejercicio práctico disponible

Implementa un mini framework de testing

Desbloquear ejercicios
// Implementa un mini framework de testing
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes