2016-12-22 29 views
5

Próbuję napisać test case dla metody w klasie Objective-C, która zwraca void. Metoda ta tworzy kolejny obiekt klasy AnotherClass i wywołuje metodę. Jak to przetestować?Testowanie pustych metod przy użyciu OCMock w iOS

Próbowałem użyć OCMock, aby utworzyć test tego. Utworzono próbny obiekt o nazwie AnotherClass i nazwano metodą mobileTest. Oczywiście, OCMVerify([mockObject makeNoise]) nie będzie działać, ponieważ nie ustawiam tego obiektu w kodzie. A więc, jak testować w takich przypadkach?

@interface Hello 
@end 

@implementation HelloWorldClass() 

-(void)mobileTest{ 
    AnotherClass *anotherClassObject = [AnotherClass alloc] init]; 
    [anotherClassObject makeNoise]; 
} 
@end 


@interface AnotherClass 
@end 

@implementation AnotherClass() 
-(void) makeNoise{ 
    NSLog(@"Makes lot of noise"); 
} 
@end 

przypadek testowy dla powyższego jest następujący:

-(void)testMobileTest{ 
    id mockObject = OCMClassMock([AnotherClass class]); 
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init]; 
    [helloWorldObject mobileTest]; 
    OCMVerify([mockObject makeNoise]); 
} 

Odpowiedz

2

Nie ma prostej odpowiedzi na to trochę bez wchodzenia w to, co OCMock jest przeznaczona do badania i co oznacza to projekt paradygmat.

Skrócona wersja to: Nie powinieneś tego sprawdzać w pierwszej kolejności. Testy powinny traktować testowane metody jako czarną skrzynkę i porównywać tylko dane wejściowe i wyjściowe.

Dłuższa wersja: Starasz się przetestować efekt uboczny, w zasadzie, gdyż makeNoise robi nic, że HelloWorldClass nawet rejestry nie zrobić (lub „widzi”). Lub umieścić go w bardziej pozytywny sposób: tak długo, jak makeNoise jest poprawnie testowany w testach napisanych dlaAnotherClass nie trzeba testować tego prostego połączenia.

Podany przykład może być nieco mylący, ponieważ oczywiście nie pozostawia nic sensownego do przetestowania w jednostkowym teście mobileTest, ale biorąc pod uwagę, że można również zlecić na zewnątrz proste wywołanie NSLog innej klasie w pierwsze miejsce (i testowanie połączenia NSLog jest oczywiście bezsensowne). Oczywiście rozumiem, że po prostu używasz tego jako przykładu i wyobrażasz sobie bardziej złożony inny scenariusz, w którym chcesz zapewnić, że zdarzy się określone połączenie.

W takiej sytuacji należy zawsze zadać sobie pytanie: "Czy jest to odpowiednie miejsce, aby to zweryfikować?" Jeśli odpowiedź brzmi "tak", to znaczy, że jakakolwiek wiadomość, którą chcesz przetestować, musi zostać przeniesiona do obiektu, który nie mieści się całkowicie w zakresie testowanej klasy. Na przykład może to być pojedyncza (jakaś globalna klasa logowania) lub własność klasy. Następnie masz uchwyt, obiekt, który możesz odpowiednio sfałszować (na przykład ustawić właściwość na częściowo wyśmiewany obiekt) i zweryfikować.

W rzadkich przypadkach może to doprowadzić do dostarczenia uchwytu/właściwości obiektu, aby można go było zastąpić próbą podczas testu, ale często oznacza to nieoptymalną konstrukcję klasy i/lub metody (I ' nie twierdzę, że tak jest zawsze).

Pozwól mi dostarczyć trzy przykłady z jednego z moich projektów do zilustrowania coś podobnego do sytuacji:

przykładu 1: Weryfikacja, że ​​adres URL jest otwarty (w mobilnym Safari): To jest po prostu sprawdzenie, openURL: jest zadzwonił do udostępnionej instancji NSApplication, coś bardzo podobnego do tego, co masz na myśli.Należy pamiętać, że wspólne wystąpienie nie jest „całkowicie wewnątrz badanego zakresu metod”, jak to jest pojedyncza”

id mockApp = OCMPartialMock([UIApplication sharedApplication]); 
OCMExpect([mockApp openURL:[OCMArg any]]); 
// call the method to test that should call openURL: 
OCMVerify([mockApp openURL:[OCMArg any]]); 

Zauważ, że działa to ze względu na specyfikę częściowym makiety: Nawet openURL: nie jest wywoływana na mock , ponieważ próbka ma związek z samym wystąpieniem, które jest używane w testowanej metodzie, może nadal weryfikować wywołanie.Jeżeli instancja nie byłaby pojedynczym, który nie działał, nie byłby w stanie utworzyć ten sam obiekt, który jest używany w tej metodzie

Przykład 2: Dopasowana wersja kodu zezwalająca na "gra" bbing "obiekt wewnętrzny.

@interface HelloWorldClass 
@property (nonatomic, strong) AnotherClass *lazyLoadedClass; 
@end 

@implementation HelloWorldClass() 
// ... 
// overridden getter 
-(AnotherClass *)lazyLoadedClass { 
    if (!_lazyLoadedClass) { 
     _lazyLoadedClass = [[AnotherClass alloc] init]; 
    } 
    return _lazyLoadedClass; 
} 
-(void)mobileTest{ 
    [self.lazyLoadedClass makeNoise]; 
} 
@end 

A teraz test:

-(void)testMobileTest{ 
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init]; 
    id mockObject = OCMPartialMock([helloWorldObject lazyLoadedClass]); 
    OCMExpect([mockObject makeNoise]); 
    [helloWorldObject mobileTest]; 
    OCMVerify([mockObject makeNoise]); 
} 

Sposób lazyLoadedClass może być nawet w rozszerzeniu klasy, to znaczy "prywatny". W takim przypadku po prostu skopiuj odpowiednią kategorię do górnej części pliku testowego (zwykle robię to i tak, to jest IMO, ważny przypadek "testowania prywatnych metod"). Takie podejście ma sens, jeśli AnotherClass jest bardziej skomplikowany i wymaga skomplikowanej konfiguracji lub czegoś podobnego. Zwykle takie rzeczy prowadzą do scenariusza, który masz w pierwszej kolejności, tj. Jego złożoność sprawia, że ​​jest czymś więcej niż tylko pomocnikiem, niż można go wyrzucić po zakończeniu metody. to również doprowadzi cię do lepszej struktury kodu, ponieważ masz inicjator w osobnej metodzie i możesz to odpowiednio przetestować.

Przykład 3: Jeśli AnotherClass ma niestandardową inicjator (jak pojedyncza, czy pochodzi on z klasy fabrycznej) można skrótową że i zwracają szydzili obiektu (jest to rodzaj mózgu węzeł, ale Użyłem go)

@implementation AnotherClass() 
// ... 
-(AnotherClass *)crazyInitializer { // this is in addition to the normal one... 
    return [[AnotherClass alloc] init]; 
} 
@end 

-(void)testMobileTest{ 
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init]; 
    id mockForStubbingClassMethod = OCMClassMock([AnotherClass class]); 
    AnotherClass *baseForPartialMock = [[AnotherClass alloc] init]; 
    // maybe do something with it for test settup 
    id mockObject = OCMPartialMock(baseForPartialMock); 
    OCMStub([mockForStubbingClassMethod crazyInitializer]).andReturn(mockObject); 
    OCMExpect([mockObject makeNoise]); 
    [helloWorldObject mobileTest]; 
    OCMVerify([mockObject makeNoise]); 
} 

to wygląda niby głupi i muszę przyznać, że to zwykły brzydki, ale użyłem tego w niektórych testach (wiesz, że punkt w projekcie ...). Tutaj starałem się ułatwić czytanie i wykorzystałem dwie mocks, jedną metodę kodującą metodę klasy (tj. Inicjator) i taką, która jest następnie zwracana. Metodę mobileTest powinno oczywiście używać niestandardowego inicjalizatora AnotherClass, a następnie pobiera wyśmiewany obiekt (jak jajko kukułki ...). Jest to przydatne, jeśli chcesz specjalnie przygotować obiekt (dlatego użyłem tu częściowej makiety). W rzeczywistości nie jestem pewien, czy mógłbyś to zrobić za pomocą tylko jednej sztuczki klasowej (użyj metody klasy/inicjalizatora na niej, aby się zwrócił, a potem spodziewaj się wywołania metody, którą chcesz zweryfikować) ... jak powiedziałem, mózg zawiązywanie.