Mocks & Stubs : La suite
Voici la suite de mon article précédent, « Leurre des tests » … Nous allons passer rapidement en revue les différents outils à notre disposition sur d’autres plate-formes pour exploiter les Mocks et les Stubs.
EasyMock (Java)
EasyMock propose d’englober l’interface pour créer votre Mock et d’effectuer les vérifications directement sur le Mock :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MailService mailer = createMock(MailService.class);
// On s'attend à ce que la méthode soit appelée avec comme paramètre le message msg
expect(mailer.send(msg));
replay(mailer);
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
verify(mailer);
}jMock (Java)
jMock fonctionne par création d’un contexte devant être satisfait. Je trouve la façon d’écrire les vérifications (partie Expectations) plutôt intéressante :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MailService mailer = context.mock(MailService.class);
// On définit les attentes
context.checking(new Expectations() {{
oneOf(mailer).send(msg);
}};
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
context.assertIsSatisfied();
}Mockito (Java, Python)
Mockito permet de fonctionner en mode Mock ou en mode Stub, ce qui est intéressant selon le contexte. Voici la version Java du code :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MailService mailer = mock(MailService.class);
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
// On vérifie l'appel
verify(mailer).send(msg);
}Et voici la version Python :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
mailer = Mock()
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)
# On vérifie l'appel
verify(mailer).send(msg)SevenMock (Java)
Ce framework m’a apparu assez souple mais très verbeux comparé à certains autres. Les vérifications passent par un contrôleur central :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MockController mockControl = new MockController();
MailService mailer = mockControl.getMock(MailService.class);
mockControl.expect(new MailService() {
public void send(Message _msg) { // La méthode send doit être appelée
assertEquals(msg, _msg); // On vérifie le paramètre
}
});
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
// On vérifie l'appel
mockControl.verify();
}JMockit (Java)
JMockit utilise un concept intéressant en écrivant les attentes directement dans une classe Mock. La méthode de test sert à décrire les actions à exécuter, tandis que la classe de Mock va vérifier les paramètres, le nombre d’appels, etc… :
public void testMemberMailSentWhenSubscribed() {
// Mise en place des Mocks
Mockit.setUpMocks(MailServiceMock.class);
// Création d'un membre
Member member = new Member("login", "password");
// On enregistre le membre
MyApp.register(member);
}
@MockClass(realClass = MailService.class)
public static class MailServiceMock {
@Mock(invocations = 1)
public void send(Message msg) {
assertEquals(new Message("Bienvenue login !"), msg);
}
}rMock (Java)
rMock enregistre les actions à partir du moment où startVerification() est appelée. J’ai eu un peu de mal avec ce framework que je trouve moins intuitif que la plupart des autres :
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MailService mailer = (MailService) mock(MailService.class);
mailer.send(msg);
startVerification();
MyApp.setMailer(mailer);
// On enregistre le membre
MyApp.register(member);
}Unitils (Java)
Unitils est un framework très intéressant en ce qui concerne les tests des DAO (accès aux bases de données). Un prochain article étudiera les différentes façons de tester une application accédant à une base de données (bien que passer par les Mock soit souvent une meilleure pratique). Le framework n’est pas spécialisé dans les Mock objects mais sait travailler avec.
La procédure est la suivante, ajouter un champ à la classe de test, qui se chargera d’instancier le Mock elle-même, puis travailler avec des assertions sur les interactions :
Mock<MailService> mockMailer;
public void testMemberMailSentWhenSubscribed() {
// Création d'un membre
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MyApp.setMailer(mockMailer.getMock());
// On enregistre le membre
MyApp.register(member);
// On vérifie l'appel
mockMailer.assertInvoked.send(msg);
}Ludibrio (Python)
Ludibrio permet de prendre tous les « Test Doubles » (Mocks, Stubs et Spy, Fakes et Dummy Objects). La documentation est par contre assez pauvre et je ne suis pas sûr d’avoir bien compris le fonctionnement, voici tout de même un exemple de ce que j’ai pu comprendre :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
with Mock() as mailer:
mailer.send(msg) << 1
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)The Python Mock Module (Python)
Python étant un langage plus dynamique que Java, la façon d’utiliser les Mocks est à vrai dire totalement différente. Le Python Mock Module permet de rajouter dynamiquement des attentes sur la classe de Mock et lèvera une exception si l’attente n’est pas respectée :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
mailer = Mock({}, MailService)
mailer.mockSetExpectation('send', expectParam(0, IS(msg)))
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)PyMock (Python)
PyMock permet aux classes de test d’hériter du classique TestCase pour rester cohérent avec d’autres tests, ou alors d’hériter directement de PyMockTestCase pour simplifier l’utilisation des Mocks :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
mailer = self.mock()
mailer.send(msg)
# Le replay indique qu'à partir de là on va vérifier que les appels précédents sont répétés
self.replay()
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)
# Vérifie l'appel
self.verify()Mocker (Python)
Mocker fonctionne à peu près comme PyMock, mais je trouve que les possibilités sont plus grandes en terme d’assertions par exemple :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
mailer = self.mocker.mock()
mailer.send(msg)
mocker.replay()
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)
# Vérifie l'appel
self.mocker.restore()
self.mocker.verify()Pymox (Python)
J’aime beaucoup PyMox, que je trouve moins intrusif que les autres outils Python. Il permet d’utiliser une factory (Mox) ou d’instancier directement les objets Mock. Un usage avancé est possible, je vous laisse regarder la documentation. Voici l’utilisation en mode factory :
def test_member_mail_sent_when_subscribed(self):
# Création d'un membre
member = Member("login", "password")
msg = Message("Bienvenue login !")
# On met en place le Mock
mocker = mox.Mox()
mocker.CreateMock(MailService)
mailer = self.mocker.mock()
mailer.send(msg)
mocker.ReplayAll()
MyApp.mailer = mailer
# On enregistre le membre
MyApp.register(member)
# Vérifie l'appel
mocker.VerifyAll()Google Mock (C++)
Le géant du web s’investit aussi dans les frameworks de Mocking (en plus du reste). Comme le C++ n’était pas fait pour ça à la base, ils ont fait un énorme travail en l’adaptant pour rendre l’écriture des tests assez simple et intuitive. Même si on reste très loin de la flexibilité du Python (C++ oblige), les possibilités sont énormes.
Voici notre interface MailService réécrite en C++ :
class MailService {
public:
virtual ~MailService();
virtual void send(Message &message) = 0;
}Le Mock Object, avec la syntaxe particulière de Google Mock :
class MailServiceMock : MailService {
public:
// Un argument de type Message&, retour de type void
MOCK_METHOD1(send, void(Message &message));
}Et le test :
TEST(MailServiceTest, MailSentWhenSubscribed) {
Member member = new Member("login", "password");
Message msg = new Message("Bienvenue login !");
// On met en place le Mock
MyApp.setMailer(&mailer);
EXPECT_CALL(mailer, send(&msg))
.Times(1);
// On enregistre le membre
MyApp.register(&member);
// Libération de la mémoire
delete member;
...
}
int main(int argc, char** argv) {
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}Mocha (Ruby)
Mocha permet d’utiliser les Mocks et les Stubs. Ne m’y connaissant pas spécialement en Ruby, je me suis peut-être trompé dans l’écriture des tests ; cependant, je trouve la syntaxe très épurée et surtout très lisible :
class MailServiceTest < Test::Unit::TestCase
def test_member_mail_sent_when_subscribed
member = Member.new("login", "password")
msg = Message.new("Bienvenue login !")
mailer = mock()
mailer.expects(:send).with(:msg)
MyApp.mailer = mailer
MyApp.register(member)
end
endFlexmock (Ruby)
J’aime beaucoup FlexMock, qui s’intègre facilement avec les frameworks de test Ruby (Test::Unit et RSpec). La syntaxe est intéressante et l’API très complète. A première vue je le trouve plus exhaustif que Mocha :
class MailServiceTest < Test::Unit::TestCase
def test_member_mail_sent_when_subscribed
member = Member.new("login", "password")
msg = Message.new("Bienvenue login !")
mailer = flexmock("mailer")
mailer.should_receive(:send).with(msg).times(1)
MyApp.mailer = mailer
MyApp.register(member)
end
endnMock (.Net)
Changeons encore d’univers pour assouvir notre soif d’apprendre. Cette fois-ci je vais écrire la classe de test complète afin de voir l’intérêt du « setUp ». Je trouve la syntaxe de nMock très propre, permettant de garder un code très structuré :
Réécrivons notre interface MailService :
public interface IMailService
{
void Send(Message message);
}Et notre classe de test :
[TestFixture]
public class MailServiceTest
{
private Mockery mocks;
private IMailService mailer;
private Member member;
private Message message;
[SetUp]
public void SetUp()
{
mocks = new Mockery();
mailer = mocks.NewMock();
member = new Member("login", "password");
msg = new Message("Bienvenue login !");
MyApp.setMailer(mailer);
}
[Test]
public void MemberMailSentWhenSubscribed()
{
Expect.Once.On(mailer).Method("Send").With(msg);
MyApp.register(member);
mocks.VerifyAllExpectationsHaveBeenMet();
}
}Moq (.Net)
Moq, le concurrent de nMock, se veut plus minimaliste dans sa syntaxe. Je ne l’ai pas étudié en détail, le trouvant moins utile que nMock. J’ai cependant vu qu’il était utilisé par exemple dans les tests unitaires d’ASP.Net MVC.
var mailer = new Mock();
// ...
mock.Verify(mailer => mailer.Execute("Send"), Times.Once());
MyApp.register(member);Il en existe sûrement des tas d’autres, pour divers autres langages (plus ou moins exotiques), mais l’intérêt de cet article était de vous montrer cette pratique des tests unitaires qui peine parfois à s’imposer face aux pratiques courantes des frameworks xUnit.
Et vous, que pensez-vous de cette technique ? En connaissez-vous d’autres similaires ou plus efficaces ?