Mock objekty se dají vytvářet několika způsoby. Můžeme je psát ručně, ale to bývá v mnoha případech nepraktické nebo zcela zbytečné. Na trhu totiž existuje mnoho knihoven s volně šiřitelným kódem, které se starají o automatické generování mock objektů. Podle mnoha odborníků je v součastnosti na trhu nejlepší knihovnou knihovna jMock od autorů webových stránek mockobjects.com[9], které se zabývají právě technikou mockování objektů. Tuto knihovnu používáme v naší aplikaci a její API se objeví ve všech následujících příkladech.
jMock je testovací rámec, který poskytuje jednoduché API pro napodobování objektů na základě jejich rozhraní. jMock umožňuje definovat požadavky na volání metod a nastavovat vlastnosti jejich chování.
Hlavní vstupní branou do jMock API je třída
MockObjectTestCase, která rozšiřuje třídu
TestCase z testovacího rámce jUnit. MockObjectTestCase poskytuje
metody pro snadné definování požadavků na interakce testovaného
objektu s jeho nejbližšími sousedy a automatické ověřování mock
objektů na konci testu.
Mock objekt, který reprezentuje rozhraní definující chování
souseda, se kterým testovaný kód komunikuje, vytvoříme konstruktorem
new Mock(Rozhrani.class) nebo pomocnou
metodou mock(Rozhrani.class), poskytovanou
objektem MockObjectTestCase. Tento mock objekt
může být přetypován na rozhraní, které napodobuje, a může tak nahradit
samotnou implementaci tohoto rozhraní v testovaném kódu.
private Mock mockUserDao = new Mock(UserDAO.class); private UserManager userManager = new UserManagerImpl(); .... userManager.setUserDAO((UserDAO)mockUserDao.proxy());
Mock objekty poskytují metody pro nastavení tzv. očekávání. jMock rozlišuje dva druhy těchto metod. Jedná se o metody typu pahýl a očekávání. Nyní si tyto druhy metod představíme podrobněji.
Pahýl je v tomto případě druh metody modelového objektu, která může ale nemusí být volána v průběhu testu. V obou případech je test úspěšný. Očekávání je naproti tomu druh metody, na kterou jsou kladeny jisté požadavky a pokud tyto požadavky nejsou naplněny, test není úspěšný. Na metodu můžeme klást požadavek, že bude volána právě jednou v průběhu testu. Při verifikaci mock objektu v závěrečné fázi testování se ověřuje, zda-li tomu tak opravdu bylo a pokud ne, potom je vyvolána chyba a test není úspěšný.
Jak tyto metody používáme? Metody typu pahýl používáme
především k získání informací o stavech objektů s tím, že tyto
metody nemají žádný vliv na stav světa, zatímco metody typu
očekávání jsou příkazy, které takový vliv mají. Metody typu
očekávání mohou mít také návratovou hodnotu. Pahýlové metody mohou
být volány nesčetněkrát, očekávání mohou být volána pouze tolikrát,
kolikrát je nastavíme, jak ukazuje následující příklad, který je
částí jednotkového testu UserManagerTest, kde
mockLogger a mockUserDao jsou
mock objekty, na kterých jsou definovány metody typu pahýl a
očekávání.
public static final String USERNAME = "sportoviny";
....
//vyvolání metody typu pahýl
mockLogger.stubs().method("getLoggingLevel").noParams()
.will(returnValue(Logger.WARNING));
//právě jedno volání metody typu očekávání zajišťuje metoda once()
mockUserDao.expects(once())
.method("getUserByUsername")
.with(isA(String.class))
.will(returnValue(user));
//přinejmenším jedno volání metody typu očekávání
mockUserDao.expects(atLeastOnce()) ...
//právě pětkrát vyvolání metody typu očekávání
mockUserDao.expects(exactly(5)) ...
....
Na parametry volaných metod můžeme klást různá omezení. Pokud
bychom na metodu getUserByUsername() kladli
požadavek, že tato metoda má být vždy volána s parametrem rovným
libovolnému objektu, který reprezentuje libovolnou hodnotu, naše
definice by byla následující.
mockUserDao.expects(once())
.method("getUserByUsername")
.with(ANYTHING)
.will(returnValue(user));
Cítíte asi sami, že toto chování není takové, jaké od této metody očekáváme. My potřebujeme tuto metodu volat s parametrem typu String, který reprezentuje uživatelské jméno registrovaného uživatele. Proto toto očekávání upřesníme a metodu definujeme následovně.
mockUserDao.expects(once())
.method("getUserByUsername")
.with(isA(String.class))
.will(returnValue(user));
Třída
MockObjectTestCase definuje mnoho typů
omezení, které můžeme po parametrech volaných metod vyžadovat. Pokud
by nám tato sada omezení nestačila, můžeme si nadefinovat vlastní
omezení
Na metody mock objektů můžeme stanovovat pořadí, v jakém mají
být volány, a to jak na pahýly tak očekávání. Následující příklad je
opět část kódu UserManagerTest, který
definuje pořadí vyvolávání metod typu očekávání.
mockUserDao.expects(once())
.method("setUser")
.with(same(user))
.isVoid().id("setting user");
mockUserDao.expects(once())
.method("getAuthorityByName")
.with(eq(new String("ROLE_CLIENT")))
.after("setting usser")
.will(returnValue(new Authority("ROLE_CLIENT")));
mockMailManager.expects(once())
.method("sendActivationEmail")
.with(new Constraint[] {isA(String.class),
isA(String.class), eq(new Locale("cs_CZ"))})
.after("setting user")
.isVoid();
Kód zachycuje volání tří metod typu očekávání, kdy metody
getAuthorityByName() a
sendActiovationEmail() jsou volány až po
metodě setUser(). Jejich vzájemné pořadí
není rozlišeno. Pokud by toto pořadí nebylo dodrženo, test by nebyl
úspěšný.
Ostatní metody z našich příkladů, jmenovitě
method(), with(),
after(), will()
definují porovnávací pravidla, která validují příchozí volání metod
mock objektu a testují jejich očekávání. Pokud by pravidla
porovnávání nevyhovovala našim potřebám, můžeme si vytvořit pravidla
vlastní.
Metoda method() objektu
MockObjectTestCase definuje jméno volané
metody mock objektu, na který je aplikována. Metoda
with() aplikuje omezení na parametry volané
metody, metoda after() určuje pořadí volání
metod mock objektů a will() určuje
návratovou hodnotu volané metody.
Nyní si ukážeme, jak připravit jednoduchý jednotkový test za použití techniky mockování objektů a nástroje jMock.
Na následujícím komentovaném příkladu si ilustrujeme techniku
mockování objektů. V podkapitole 4.5. jsme se zmínili o určitém
scénáři vývoje interakčních jednotkových testů. Totoho scénáře se
budeme držet i v tomto příkladu. Popíšeme si tvorbu jednotkového testu
implementace třídy UserManager a jeho metody
newRegistratedUserAndSendActivationEmail().
Plné znění tohoto testu najdete v souboru
UserManagerTest.java.
public class UserManagerMockTest extends MockObjectTestCase {
/* definice mock objektů */
private Mock mockUserDao;
private Mock mockMailManager;
...
/* definice testovaného objektu */
private UserManager userManager;
...
/* nastavení proměnných */
public static final String USERNAME = "sportoviny";
...
/* instanciace a nastavení testu */
protected void setUp() throws Exception {
super.setUp();
mockUserDao = new Mock(UserDAO.class);
mockMailManager = new Mock(MailManager.class);
userManager = new UserManagerImpl();
userManager.setUserDAO((UserDAO)mockUserDao.proxy());
userManager.setMailManager((MailManager)mockMailManager.proxy());
...
user = new User();
user.setUsername(USERNAME);
...
}
/** otestuje uložení uživatele a zaslání aktivačního mailu */
public void testSaveNewRegistratedUserAndSendActivationEmail() {
/* definice pojmenovaného očekávání jednoho
* volání metody setUser() s parametrem typu User a
* návratovou hodnotou void
*/
mockUserDao.expects(once())
.method("setUser")
.with(isA(User.class)
.isVoid().id("setting user");
/* definice pojmenovaného očekávání jednoho volání
* metody getAuthorityByName()
* s parametrem typu String a hodnotou "ROLE_CLIENT" a
* návratovou hodnotou jako instancí třídy Authority, která
* je volána po metodě setUser()
*/
mockUserDao.expects(once())
.method("getAuthorityByName")
.with(eq(new String("ROLE_CLIENT")))
.after("setting usser")
.will(returnValue(new Authority("ROLE_CLIENT")));
...
/* vyvolání metody doménového objektu */
userManager.saveNewRegistratedUserAndSendActivationEmail(user,
new String("nejaka/url"), new Locale("cs_CZ"));
/* verifikace nastavených očekávání */
mockUserDao.verify();
mockMailManager.verify();
...
}
....
}
Již několikrát jsme si řekli, že používáme-li mock objekty pro psaní jednotkových testů, pak produkujeme kód, který z hlediska návrhu vyhovuje těm nejpřísnějším normám. Nicméně používání techniky mockování objektů nenahrazuje zkušenosti kvalitního vývojáře. Zkušenosti ukazují, že interakční jednotkové testy se stávájí příliš komplikované v případě, že návrh systému je špatný, a zesilují tak problémy přílišné závislosti kódu. Následující části textu popisují dobré rady, které nám mohou pomoci v návrhu naší aplikace a umožní nám tak psát jednoduší a srozumitelné jednotkové interakční testy.