Estas preguntas me vienen constantemente, y hoy fue una de esas veces… Estuve trabajando en https://www.briq.mx/ construyendo una estructura de datos que debería tener información de varias fuentes de la aplicación.

En general debía recopilar algunos datos que teníamos en la base de datos, pero también información de cálculos que hacíamos “en el aire” en otros lados de la aplicación.

Yo tiendo a estructurar la aplicación en módulos y en este caso que les quiero contar estaba ocupando información del módulo UserPortfolio en el módulo Users.

El primer intento…

Al principio al estar realizando las pruebas de esta nueva función, empece diseñando las entradas de la función en base a la información que guardamos en la base de datos. Es decir que dentro de esta nueva función estaba probando de nuevo el comportamiento de las funciones que usaría de UserPortfolio.

Esto tiene sus puntos buenos como….

Cuando cambie la interfaz o el comportamiento de UserPortfolio las pruebas me dirían directamente si algo se rompió dentro de la interacción.

y sus punto malos como…

Si algo cambia dentro de UserPortfolio tendía que cambiarlo en dos partes.

Probar UserPortfolio dos veces es muy complejo porque requiere probar muchos casos borde que son muy difíciles de recordar.

Si no pruebo exhaustivamente los casos borde, cuando llegue otra persona al proyecto (que puedo ser yo en unos meses) puede llegar a considerar que es más fácil re-implementar cierta funcionalidad que interactuar con otro módulo.

Dentro de todo esto creo que el último punto es el más peligroso… porque dentro del código hay ciertas decisiones que han sido tomadas después de varios casos de uso específicos que son difíciles de pensar y reproducir.

El segundo intento…

Después de considerar esos puntos, pensé que tal vez puesto que son módulos diferentes podría hacer un stub de la interfaz que necesitaba y esperar que un objeto con esa interfaz fuera un parámetro de mi nueva función.

Esto suena bien, pero también tiene sus puntos malos como…

El UserPortfolio es parte core del sistema y esta en constante cambio, por lo que depender a la ligera de cualquier estructura que regrese alguna función del módulo sería arriesgado (sobre todo si trabajas con un lenguaje que no tiene tipos).

Lo bueno es que aún en un leguaje no tipado como Ruby, podemos ayudarnos de las pruebas para asegurarnos que por lo menos la interfaz se va a mantener.

Sandy Metz en su libro Practical Object-Oriented Design in Ruby habla un poco de este tema. La idea es hacer un conjunto de pruebas compartidas que puedan ser corridas fácilmente aún para los mocks que utilizas en tus pruebas.

Aquí va un ejemplo…

RSpec.shared_examples "investments value interface" do
  it { expect(subject).to respond_to(:principal_invested) }
  it { expect(subject).to respond_to(:generated_value) }
  it { expect(subject).to respond_to(:current_value) }
  it { expect(subject).to respond_to(:has_current_value_with_adjustment?) }
  it { expect(subject).to respond_to(:value_of_received_payments) }
  it { expect(subject).to respond_to(:count_of_received_payments) }
end

y esto lo puedes usar fácilmente para probar tu código de producción…

def portfolio_with(*projects, current_date)
  projects_finder = FakeProjectFinder.new(*(projects + [refunded_project]))
  UserPortfolio.portfolio_for_user(user, current_date, projects_finder)
end

describe "implements investments value interface" do
  subject { portfolio_with(project) }
  it_behaves_like "investments value interface"
end

y para asegurarte que tus Mocks cumplen con la interfaz esperada…

def portfolio_with(attrs)
  FakePortfolio.new(attrs)
end

class FakePortfolio
  attr_reader :projects_with_investment, :principal_invested,
    :current_value, :generated_value, :value_of_received_payments,
    :count_of_received_payments

  def initialize(attrs)
    @projects_with_investment = attrs[:projects_with_investment] || []
    @principal_invested = attrs[:principal_invested] || 0
    @current_value = attrs[:current_value] || 0
    @generated_value = attrs[:generated_value] || 0
    @value_of_received_payments = attrs[:value_of_received_payments] || 0
    @count_of_received_payments = attrs[:count_of_received_payments] || 0
  end

  def has_current_value_with_adjustment?
  end
end

describe "fake portfolio implements investments value interface" do
  subject { portfolio_with({}) }
  it_behaves_like "investments value interface"
end

Tal vez esto pueda parecer excesivo para algunos y tal vez para los que vienen de lenguajes tipados puede parecer como trabajo que el lenguaje debería hacer por ti.

Yo creo que es una de las bondades de ruby el tener la flexibilidad de protegerte de cambios en la interfaz cuando realmente lo crees necesario.

… y esto es todo por hoy, espero que esta información te sea de utilidad =)