2014-05-17 22 views
10

Piszę testy Rspec dla obiektu usługi, który dotyka kilku modeli, ale mam wrażenie, że mój test jest zbyt zależny od elementów wewnętrznych metody i dlatego nie jest zbyt znaczący. Oto przykład:Porady Rspec dotyczące testowania obiektów usługowych

class MealServicer 

    def self.serve_meal(meal, customer) 
    meal.update_attributes(status: "served", customer_id: customer.id) 
    order = customer.order 
    OrderServicer.add_meal_to_order(meal, order) 
    CRM.update_customer_record(customer) // external API call 
    end 

end 

Chciałbym użyć podwójnych/odcinki drwić zachowanie faktycznie bez zapisywania czegokolwiek do testowej bazy danych (dla wydajności). Ale jeśli tworzę podwójne, które odpowiadają na wiadomości, to wydaje mi się, że testuję jedną konkretną implementację metody serve_meal(), a ten test jest zbyt sprzężony z tą konkretną implementacją. Na przykład, muszę się upewnić, że mój customer double odpowiada na order i zwraca kod order. Zasadniczo, kiedy wszystko jest po prostu podwójne i muszę wyraźnie określić wszystkie zależności, upewniając się, że debile zwracają inne debla, wydaje się, że testy kończą się bezsensownie. Zobacz tutaj:

it "has a working serve_meal method" do 
    meal = double(:meal) 
    customer = double(:customer) 
    order = double(:order) 

    allow(customer).to_receive(:order).and_return(order) 
    allow(OrderServicer).to_receive(:add_meal_to_order).and_return(true) 
    allow(CRM).to_receive(:update_customer_record).and_return(true) 

    expect(meal).to receive(:update_attributes).once 
    expect(OrderServicer).to receive(:add_meal_to_order).once 
    expect(CRM).to receive(:update_customer_record).once 
end 

Czy jest jakiś inny sposób na sprawdzenie tego dokładnie i sensownie, inne niż rzeczywiste instancji posiłku, klientów oraz przedmiotów zamówień związanych odpowiednio (i ewentualnie zapisane w datbase), a następnie sprawdzić, czy MealServicer.serve_meal (...) aktualizuje właściwości obiektu zgodnie z oczekiwaniami? To ostatecznie spowoduje zapisanie do bazy danych, ponieważ update_attributes wykonuje wywołanie zapisu, a więc kilka metod, które zamierzam uwzględnić w metodzie obiektu usługi.

Wreszcie, ponieważ testy zależą od implementacji, nie mogę napisać testów przed metodą, co zalecają zwolennicy TDD. To po prostu czuje się do tyłu. Wszelkie porady dotyczące pisania wydajnych, ale użytecznych testów?

+0

Tak jesteś w zasadzie stubbing najbardziej rzeczywistego kodu, więc nie będą tak naprawdę sprawa metoda zawiedzie, chyba że faktycznie z niego usuwać wierszy . Biorąc pod uwagę naturę obiektu, ten rodzaj logiki biznesowej jest czymś, co testowałem bardziej na poziomie integracji zamiast testu jednostkowego. Masz do czynienia z wieloma obiektami, które robią różne rzeczy. I naprawdę czytając twoją metodę, to brzmi jak cecha spec. Byłbym ciekawy, jak wykorzystujesz MealServicer w aplikacji. Ale nie martw się o używanie tych rzeczywistych obiektów.Napisz test na to, czego się spodziewasz po wywołaniu tej metody – agmcleod

Odpowiedz

17

Jest to dylemat "Mockist vs Classicist", o którym mowa w artykule Martina Fowlera: Mocks Aren't Stubs. Używanie mocks (podwójnych) przez cały proces będzie wymagało użycia innych metod dla współpracowników i ujawnienia implementacji. Jest to część ceny, którą płacisz za szybkość i elastyczność szyderstwa.

Inną kwestią jest to, że nie ma naturalnego "przedmiotu" dla specyfikacji, ponieważ jest to metoda klasy. W efekcie otrzymujesz trzy obiekty, które należy zaktualizować; w pewnym sensie są to na przemian podmioty i współpracownicy w zależności od tego, jakie oczekiwania są wykonywane. Można zrobić to bardziej oczywiste, poprzez ustawienie jednego oczekiwanie na przykład:

describe MealServicer do 
    context ".serve_meal" do 
    let(:order) { double(:order) } 
    let(:meal) { double(:meal) } 
    let(:customer) { double(:customer, id: 123, order: order } 

    it "updates the meal" do 
     allow(OrderServicer).to_receive(:add_meal_to_order) 
     allow(CRM).to_receive(:update_customer_record) 
     expect(meal).to receive(:update_attributes).with(status: "served", customer_id: 123) 
     MealServicer.serve_meal(meal, customer) 
    end 

    it "adds the meal to the order" do 
     allow(meal).to receive(:update_attributes) 
     allow(CRM).to_receive(:update_customer_record) 
     expect(OrderServicer).to receive(:add_meal_to_order).with(meal, order) 
     MealServicer.serve_meal(meal, customer) 
    end 

    it "updates the customer record" do 
     allow(meal).to receive(:update_attributes) 
     allow(OrderServicer).to_receive(:add_meal_to_order) 
     expect(CRM).to receive(:update_customer_record).with(customer) 
     MealServicer.serve_meal(meal, customer) 
    end 
    end 
end 

Teraz odcinki zawsze są zależne, a oczekiwania są rzeczy są testowane, wyjaśniającym intencje spec.

ponieważ badania zależy od implementacji, nie mogę pisać testy przed metody

nie zgadzam. Jeśli oddzielisz oczekiwania, możesz najpierw przetestować i napisać kod, aby testy minęły, jeśli pracujesz na jednym przykładzie na raz.

EDIT

patrz również ten blog post przez Myrona Marston

Powiązane problemy