27.5.05

Os testes e o modelo de dados.

O modelo de dados é certamente uma das partes mais importantes de qualquer projecto, e é geralmente o ponto de partida da fase de desenvolvimento. Também é comum destacar-se uma camada para aceder a esses mesmos dados. No caso do Java, a maioria das vezes, utiliza-se o modelo ‘MVC’, cabendo à camada ‘Model’ esta tarefa. Nas arquitecturas de .Net a camada equivalente é denominada de ‘DataAccess’.

Quem utiliza métodos ágeis depara sempre com o mesmo problema no início de qualquer projecto: “Como é que vou testar a camada de acesso aos dados sem introduzir alterações na base de dados de testes?”
Esta dúvida surge porque, se estamos a testar a inserção dos dados de clientes num teste unitário, e noutro a verificar o número de clientes que existem no sistema, é certo que o segundo vai falhar se correr depois do primeiro. Desta forma está-se a violar o princípio do ‘Isolamento’.
Neste artigo irei sugerir uma técnica que permite testar a interacção entre o nosso programa e o modelo de dados, sem que se viole o isolamento dos testes de regressão.
A filosofia desta técnica consiste em registar todos os acessos numa única transacção, e depois de verificadas todas as condições, forçar o ‘rollback’ da mesma.

Por questões de simplicidade o exemplo será apresentado em Java recorrendo a uma ligação JDBC para MySql, mas a sua utilização em C# ou noutra linguagem orientada aos objectos é análoga. Gostaria apenas de realçar que para se utilizar esta técnica é necessário que o ‘DBMS’ ou a ‘Framework’ suporte transacções. Para uma arquitectura em Java com MySql é obrigatória a utilização de tabelas do tipo ‘INNODB’ por ser o único que suporta transacções. Alternativamente pode-se recorrer a um contentor de ‘EJBs’ que garanta a transacção ao nível da ‘Framework’.
Para a tecnologia .Net a minha preferência resume-se à utilização do ‘Distribucted Transaction Manager’, e efectuar manualmente a gestão das transacções.

Em primeiro lugar é necessário encapsular a ligação à base de dados utilizada, desta forma ficaremos com a classe:


public class Database implements IDatabase
{
private Connection connection;
private boolean transactionStarted = false;

public Database(String databaseName, String username, String password)
throws ClassNotFoundException, SQLException
{
Class.forName("org.gjt.mm.mysql.Driver");
connection = DriverManager.getConnection(...);
}


A nossa classe irá implementar um Interface que apresenta todos os seus métodos públicos, e a ligação à base de dados é feita no construtor. A forma como a ligação é feita é irrelevante, e pode-se optar por ler os dados da ligação de um ficheiro de configuração.


public Connection getConnection()
{
return connection;
}

public void beginTransaction()
throws TransactionAlreadyStartedException, SQLException
{
if (transactionStarted)
throw new TransactionAlreadyStartedException();
setTransactionStarted(true);
connection.setAutoCommit(false);
}

public void commitTransaction()
throws TransactionNotStartedException, SQLException
{
if (!transactionStarted)
throw new TransactionNotStartedException();
connection.commit();
connection.setAutoCommit(true);
setTransactionStarted(false);
}

public void rollbackTransaction()
throws TransactionNotStartedException, SQLException
{
if (!transactionStarted)
throw new TransactionNotStartedException();
connection.rollback();
connection.setAutoCommit(true);
setTransactionStarted(false);
}

public boolean inTransaction()
{
return transactionStarted;
}


O método getConnection() retorna a ligação que se está a utilizar, mas é importante garantir que o nosso código não vai executar métodos que afectem o estado das transacções. Eu optei por esta forma por questões de simplicidade, mas se quisermos ser rigorosos teremos que utilizar uma técnica em que a ligação não seja exposta publicamente.



protected void setTransactionStarted(boolean transactionStarted)
{
this.transactionStarted = transactionStarted;
}
}


Este método será utilizado para se iniciar uma nova transacção.

Em seguida iremos criar uma 'Factory', por duas razões. Em primeiro lugar queremos garantir que apenas existirá um objecto do tipo Database no nosso programa, depois porque mais tarde iremos substituir este objecto por outro sem que o nosso programa se aperceba disso.


public class DatabaseFactory implements IDatabaseFactory
{
private IDatabase database;
private ILog log;

public ModelFactory() throws ClassNotFoundException, SQLException
{
}

public setDatabase(IDatabase database)
{
this.database = database;
}

public IDatabase getDatabase()
{
return database;
}
}


Esta classe é bastante simples, e penso que dispensa comentários.

Agora falta arranjar uma forma que permita ao nosso programa localizar a factory disponível, por forma a obter a ligação. Eu optei pelo padrão 'Service Locator', mas existem outras técnicas válidas.


public final class Services {

private static IModelFactory modelFactory;

private Services() {
}

public static IModelFactory geModelFactory()
{
return modelFactory;
}

public static void setModelFactory(IModelFactory factory)
{
modelFactory = factory;
}
}


No ponto de arranque do nosso programa procede-se à configuração da classe Services, para podermos disponibilizar as fábricas que serão utilizadas durante a execuçãp. Neste exemplo apenas temos uma 'Factory' mas é comum existir pelo menos uma por camada.

Agora que temos o nosso ambiente de desenvolvimento completo, podemos começar a trabalhar uma classe que irá aceder aos dados. No entanto, à boa maneira ágil, iremos começar pelos testes :)
O nosso objectivo é registar um cliente e a sua morada, estando estes dados em tabelas diferentes (o exemplo poderia ser melhor, mas estou com falta de imaginação). Por questões de simplicidade o cliente é apenas composto pelo seu nome, a morada pela localidade e não irei utilizar sequências nas tabelas (o nome é a chave).

Nos testes iremos substituir a classe Database por uma outra cuja transacção não seja efectuada. Assim teremos uma classe de nome DatabaseTestInstance para ser utilizada exclusivamente nos testes.

   
public class DatabaseTestInstance extends Database implements IDatabase
{
public Database(String databaseName, String username, String password)
throws ClassNotFoundException, SQLException
{
super(databaseName, username, password);
getConnection().setAutoCommit(false);
}

public void commitTransaction()
throws SQLException, TransactionNotStartedException
{
rollBackTransaction();
}

public void rollBackTransaction()
throws SQLException, TransactionNotStartedException
{
super.rollbackTransaction();
getConnection().setAutoCommit(false);
}
}


Como podemos observar, o contrutor desta classe desliga o modo 'auto-commit' e faz sempre 'rollback' quando uma transacção termina.

Passando ao teste propriamente dito temos:


public class ClientTest extends TestCase
{
private IClientModel clientModel;

public TestBase(String name) throws ClassNotFoundException, SQLException
{
super(name);
IDatabaseFactory databaseFactory = new DatabaseFactory();
modelFactory.setDatabase(
new model.tests.unit.injections.Database
("database", "user", "pass"));
Services.setModelFactory(databaseFactory);
}


Aqui pode-se observar que estamos a substituir a classe que representa a ligação por uma classe de testes. A este conceito dá-se o nome de 'Dependency Injection'.


public static junit.framework.Test suite()
{
junit.framework.TestSuite suite =
new junit.framework.TestSuite(MillionaireTest.class);
return suite;
}

protected void setUp() throws java.lang.Exception {
super.setUp();
clientModel = new ClientModel();
}

public void testInsertClient() throws SQLException {
clientModel.InsertClient("Maria", "Lisboa");

IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();
String query = "SELECT count(*) as count " +
"FROM clients, local" +
"WHERE name like 'Maria'";
statement.execute(query);
ResultSet result = statement.getResultSet();
result.next();

assertTrue("Client not inserted.", result.getInt("count") == 1);

result.close();
}


Este teste verifica a inserção de um novo cliente. O próximo testa se o número de clientes na tabela é o esperado.


public void testCountClients() throws SQLException {
assertIsTrue("Number of clients is not the expected",
clientModel.countClients() == 10);
}


Desde já pode-se verificar que se o isolamento entre os testes não se verificar, correr testInsertClient e depois testCountClients é diferente de correr testCountClients e depois testInsertClient.

Agora que temos os testes, falta apenas programar a classe ClientModel:


public class ClientModel implements IClientModel
{

private final String GET_NUMBER_CLIENTS = "SELECT count(*) as count " +
"FROM clients";

private final String INSERT_CLIENT = "INSERT INTO clients VALUES(?)";

private final String INSERT_LOCAL = "INSERT INTO local VALUES(?, ?)";

public ClientModel()
{
}

public int countClients() throws SQLException
{
IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();

statement.execute(GET_NUMBER_CLIENTS);
ResultSet result = statement.getResultSet();
result.next();

int numberOfClients = result.getInt("count");
result.close();

return numberOfClients;
}


Note-se a subtileza da declaração da variável 'database'. O valor atribuído a esta variável é o que a fábrica retornar, e é isso que torna possível substituir a ligação à base de dados sem que o programa tenha conhecimento.

Agora o método em falta:


public void insertClient(String clientName, String local)
throws SQLException, InvalidValueException, ModelException
{
IDatabase database = Services.getDatabaseFactory().getDatabase();
Statement statement = database.getConnection().createStatement();

try {
db.beginTransaction();
insertClient(clientName);
insertLocal(local, clientName);
db.commitTransaction();
}
catch (TransactionException e)
{
throw new ModelException(e);
}
}
...


Deixo por implementar os métodos insertClient e insertLocal. De qualquer forma, é possível verificar que o comportamento de db.commitTransaction() é diferente caso se esteja em testes ou a executar o programa.


Resumo:
Verificámos como é possível garantir o isolamento dos testes unitários no que diz respeito à utilização de um modelo de dados. Para isso tivemos que encapsular a ligação numa classe, utilizar uma fábrica e injectar as dependências através de um 'service locator'.
Gostaria ainda de realçar que esta técnica funciona se o nosso programa não efectuar vários pedidos ao DBMS em simultâneo (i.e. não efectuar transacções concorrentes). Além disso, se acima da camada de acesso aos dados necessitar de iniciar transacções, esta técnica deve ser aplicada nessa camada também. Para evitar problemas futuros, é aconselhável começar transacções sempre na mesma camada.

Um abraço,
Gama Franco

Sem comentários: