jeudi 14 décembre 2006
Test unitaires et Socket
La litérature déborde d'informations sur la façon de faire des test unitaires sur des classes simples et autonomes.
Lorsque l'on creuse un peu plus on arrive vite à avoir besoin d'utiliser des objets factices ou encore 'Mock Object', ce que encore une fois l'on fait facilement grâce à des librairies comme JMock, que je ne saurais trop conseiller d'ailleurs car son utilisation, une fois passé l'écueil de la documentation un peu sommaire, est assez facile.
Après avoir passé quelques heures à tester et à implémenter des classes simples, on se retrouve à implémenter les couches basses de l'application qui font appel à des librairies non conçues pour les tests unitaires (n'utilisant pas d'interfaces).
Comment procéder lorsque l'on doit tester d'une manière unitaire le code suivant ?
(Ce n'est pas forcément l'implémentation optimum d'un client TCP, mais disons que cet exemple est la pour illustrer la méthode de test :)).
public boolean sendMessage(byte message, int length) { Socket socket = null; try { socket = new socket(inetAddress,portnumber); OutputStream outPut =socket.getOutputStream(); outPut.write(message, 0, length); outPut.close(); } catch (IOException e) { return false; } return true; }
La classe Socket et OutputStream, ne sont pas des implémentations d'interface, et il est donc impossible d'utiliser JMock pour vérifier que leur appel est bien effectué, ou de simuler le comportement de problème réseau qui peuvent lever des IOException. Pourtant il serait bien pratique de le faire et éviterai d'avoir à tester manuellement les cas de connexion et déconnexion réseau en devant enlever ou remettre le câble réseau de la machine.
Première solution, on laisse tel quel et on testera à la main.
Deuxième solution, qui une fois que l'on a trouvé la façon de faire ne parait pas si difficile, ecrire soit même des object factices qui seront renvoyé par une 'SocketFactory' que l'on pourra elle même utiliser d'une manière factice grâce à JMock. Ces objets héritent des objets que l'on ne peut pas tester et l'on peut facilement surcharger les méthodes que l'on veut pouvoir simuler.
- Ecrire la classe factory qui renverra l'implémentation de la socket
public class SocketFactoryImpl implements SocketFactory { public Socket createSocket(InetAddress address, int port) throws IOException { return new Socket(address,port); } }
- Utiliser cette factory pour renvoyer la Socket à la classe utilisatrice.
protected boolean sendMessage(byte message, int length) { Socket socket = null; try { socket = socketFactory.createSocket(inetAddress, portNumber); OutputStream outPut =socket.getOutputStream(); outPut.write(message, 0, length); outPut.close(); } catch (IOException e) { return false; } return true; }
- Ecrire dans le test unitaire des classes factices Socket et OutputStream
private class MockOutputStream extends OutputStream { public boolean writeCalled = false; public boolean closeCalled = false; public boolean sendException = false; public void write(byte b, int off, int len) throws IOException { if (sendException) throw new IOException("MockOutputStream exception"); writeCalled = true; } public void close() throws IOException { closeCalled = true; } /* (non-Javadoc) * @see java.io.OutputStream#write(int) */ @Override public void write(int b) throws IOException { } } private class MockSocket extends Socket { public boolean sendException = false; public boolean closeCalled = false; private OutputStream outputStream; public void setOutputStream(OutputStream outputStream) { this.outputStream = outputStream; } public OutputStream getOutputStream() throws IOException { if (sendException) throw new IOException("mock exception"); return outputStream; } public synchronized void close() throws IOException { if (sendException) throw new IOException("mock exception"); closeCalled = true; } }
Une fois ces deux classes écrite il est facile de les utiliser pour simuler un comportement réseau hératique.
Exemple :
public void testSendMessage() { MockOutputStream mockOutputStream = new MockOutputStream(); MockSocket mockSocket = new MockSocket(); mockSocket.setOutputStream(mockOutputStream); socketFactory.expects(once()).method("createSocket").will(returnValue(mockSocket)); assertTrue ("message not sent",tcpLink.sendDataMessage(new byte32, 32)); assertTrue("output stream write not called",mockOutputStream.writeCalled); assertTrue("output stream close not called",mockOutputStream.closeCalled); assertTrue("socket close not called",mockSocket.closeCalled); } public void testSendMessageSocketException() { MockOutputStream mockOutputStream = new MockOutputStream(); MockSocket mockSocket = new MockSocket(); mockSocket.setOutputStream(mockOutputStream); mockSocket.sendException = true; socketFactory.expects(once()).method("createSocket").will(returnValue(mockSocket)); assertFalse ("message should be not sent",tcpLink.sendDataMessage(new byte32,32)); }
Cette solution est bien entendu applicable lorsque l'on a peu de cas possibles ou de méthodes à tester, dans le cas contraire le nombre de combinaisons possibles deviendrait assez élevé et la complexité de l'implémentation des objets factices rendrait les tests difficile à écrire et donc non exempts de défaut.
Il y a d'autres pistes à creuser, comme l'utilisation d'une classe de délégation ou l'utilisation d'une classe dérivée implémentant une interface similaire à la classe à simuler. Peut être à un prochain billet pour décrire ces deux méthodes.
Ce billet, écrit à 20:41 par Jean-Yves LEBLEU dans la catégorie Java a suscité :