Presentation de MockObjects par JeromeFillon et VincentTence
Cette presentation sera faite le 7 fevrier 2002. La presentation est en 2 parties. La premiere - theorique - presente le concept des mock objects et ses avantages. La deuxieme est une petite etude de cas pour mettre en pratique les concepts presentes.
-- Vincent
Deuxieme partie: Etude de cas
Points forts
L'exemple présente ci-dessous démontre l'utilite des mock pour l'abstraction des couches materielles et/ou logicielles
Cahier des charges
On veut récupérer des données de Personne en provenance d'une base de données.
Ecrire un test! PersonneTest?.java import junit.framework.*; public class PersonneTest? extends TestCase{ public PersonneTest?(String s){ super(s); } public void setUp(){ } public void tearDown(){ } public void testLoad() throws Exception{ String expected = "Jerome Fillon (Cel: 5148032991) EMail: jerome.fillon@standardlife.ca"; Personne m_Personne = Personne.loadFromDB("select * from personne"); assertEquals(expected,m_Personne.toString()); } } Personne.java public class Personne{ }
Vérifier que la compilation ne fonctionne pas.
Fixer la classe Personne.java pour que les classes compilent. Personne.java public class Personne{ public static Personne loadFromDB(String s){ return new Personne(); } }
Vérifier que le test ne fonctionne pas.
Écrire le code minimal qui fonctionne en utilisant une base de données. Personne.java import java.sql.*; public class Personne{ private static Connection m_Connection; private String m_Nom = ""; private String m_Prenom = ""; private String m_Tel = ""; private String m_EMail = ""; public Personne(String nom, String prenom, String tel, String email){ m_Nom = nom; m_Prenom = prenom; m_Tel = tel; m_EMail = email; } public static void setConnection(Connection c){ m_Connection = c; } public static Personne loadFromDB(String s){ Personne result = null; try{ Statement stmt = m_Connection.createStatement(); ResultSet rs = stmt.executeQuery(s); if (rs.next()){ result = new Personne(rs.getString(1),rs.getString(2),rs.getString(3),rs.getString(4)); } }catch (SQLException e){; }finally{ return result; } } public String toString(){ return m_Nom + " " + m_Prenom + " (Cel: " + m_Tel + ") EMail: " + m_EMail; } }Pour faire fonctionner ce code on doit :
Changement dans PersonneTest?.java pour utiliser les mockobjects. PersonneTest?.java import junit.framework.*; import com.mockobjects.*; import com.mockobjects.sql.*; public class PersonneTest? extends TestCase{ public PersonneTest?(String s){ super(s); } public void setUp(){ } public void tearDown(){ } public void testLoad() throws Exception{ //Expected values String expected = "Jerome Fillon (Cel: 5148032991) EMail: jerome.fillon@standardlife.ca"; String query = "select * from personne"; //Mock MockConnection? connection = new MockConnection?(); MockStatement? statement = new MockStatement?(); MockResultSet? rs = new MockSingleRowResultSet?( new String[]{"Jerome","Fillon","5148032991","jerome.fillon@standardlife.ca"} ); statement.setupResultSet(rs); statement.setExpectedQueryString(query); connection.setupStatement(statement); //Personne Personne.setConnection(connection); Personne m_Personne = Personne.loadFromDB(query); assertEquals(expected,m_Personne.toString()); //Verification des mocks connection.verify(); statement.verify(); rs.verify(); } }
On roule le test.Un bug a été trouvé
Le code précédent est mis en production et fonctionne bien. Cependant on note un problème au niveau du nombre de connections qui ne diminuent pas, beaucoup de mémoire est perdue.
Comment résoudre le problème ?
On écrit un test. On pense que le problème vient de l'oubli des fermetures dans les connexions? On modifie la classe PersonneTest?.java PersonneTest?.java //On ajoute la méthode suivante public void testBug() throws Exception{ //Expected values String expected = "Jerome Fillon (Cel: 5148032991) EMail: jerome.fillon@standardlife.ca"; String query = "select * from personne"; //Mock MockConnection? connection = new MockConnection?(); MockStatement? statement = new MockStatement?(); MockResultSet? rs = new MockSingleRowResultSet?( new String[]{"Jerome","Fillon","5148032991","jerome.fillon@standardlife.ca"} ); statement.setupResultSet(rs); statement.setExpectedCloseCalls(1); statement.setExpectedQueryString(query); connection.setupStatement(statement); connection.setExpectedCloseCalls(1); rs.setExpectedCloseCalls(1); //Personne Personne.setConnection(connection); Personne m_Personne = Personne.loadFromDB(query); assertEquals(expected,m_Personne.toString()); //Verification des mocks connection.verify(); statement.verify(); rs.verify(); }
On compile le test. Celui-ci doit compiler!
On vérifie que le test ne passe pas.
On fixe le code dans Personne.java pour faire passer le test. Personne.java import java.sql.*; public class Personne{ private static Connection m_Connection; private String m_Nom = ""; private String m_Prenom = ""; private String m_Tel = ""; private String m_EMail = ""; public Personne(String nom, String prenom, String tel, String email){ m_Nom = nom; m_Prenom = prenom; m_Tel = tel; m_EMail = email; } public static void setConnection(Connection c){ m_Connection = c; } public static Personne loadFromDB(String s){ Personne result = null; Statement stmt = null; ResultSet rs = null; try{ stmt = m_Connection.createStatement(); rs = stmt.executeQuery(s); if (rs.next()){ result = new Personne(rs.getString(1),rs.getString(2),rs.getString(3),rs.getString(4)); } }catch (SQLException e){; }finally{ try{ m_Connection.close(); stmt.close(); rs.close(); }catch(Exception e){ } return result; } } public String toString(){ return m_Nom + " " + m_Prenom + " (Cel: " + m_Tel + ") EMail: " + m_EMail; } }
On vérifie que le code passe le test du bug mais également tous les autres tests!Simuler un cas d''exception Supposons maintenant qu'une exception survient lors de la récupération des données. Notre code est supposé retourner une Personne nulle. Dans XP toute fonctionnalité non testée est considérée inexistante. Nous devons donc la tester.
On écrit un nouveau test. On modifie la classe PersonneTest?.java PersonneTest?.java //On ajoute la méthode suivante public void testException() throws Exception{ //Expected values Personne expected = null; String query = "select * from personne"; //Mock MockConnection? connection = new MockConnection?(); MockStatement? statement = new MockStatement?(); '' statement.setupThrowExceptionOnExecute(new SQLException("blabla")) statement.setExpectedQueryString(query); connection.setupStatement(statement); //Personne Personne.setConnection(connection); Personne m_Personne = Personne.loadFromDB(query); assertEquals(expected,m_Personne); //Verification des mocks connection.verify(); statement.verify(); }
On roule les tests, on verifie que tous les tests fonctionnent. Remarque: Pour bien faire il aurait fallu faire casser le test précédent avant de le faire passerpour vérifier que l'on fixe bien la bonne chose.Et quand on ne peut pas mocker ? Il existe un cas tres simple où l'on ne peut pas mocker.
Un objet mock est en général dérivé de la classe à mocker. ( Cependant on préfèrera utiliser les interfaces. quand c'est possible! ) Ex: public final myFunction() dans ce cas on ne peut dériver la classe et mocker cette fonction, il faut alors réaliser un enrobage (wrapper) de votre classe à mocker. Ainsi utiliser les mockobjects peut parfois conduire à modifier son code. C'est pourquoi l'approche Tests first préconisée par XP est très importante.
Un autre cas où on ne peut pas mocker: Les méthodes statiques! On ne peut pas créér un mock en dérivant de la classe à mocker et en changeant le comportement de la méthode statique (polymorphisme), dans ce cas on cache la méthode statique et donc la méthode du mock ne sera jamais appelée.
-- Jerome