Muchos de los que escriben unit test, seguramente se habrán topado con la necesidad de crear objetos para el Fixture de los diferentes tests.
Una primera solución a esta cuestión consiste en crear los objetos del fixture directamente en el test method.
Supongamos que el objeto necesario en el fixture es un Person que requiere un nombre, apellido y documento para poder existir, entonces tendríamos el siguiente código:
public void testMethod() {
Person person = new Person("Pepe", "Muleiro", new Document(Type.A, new Number(23232222)));
....
}
En cuanto aparece otro test method que requiere un person como en este caso, empezamos a notar cierta duplicación.
También, muchas veces se da que en algunos test, no nos importa el Person, solo requerimos uno para pasarlo por parámetro a algún otro objeto, o para crear otro.
Habíamos comentado en alguna charla que muchas veces esta problemática la podíamos atacar con algunos refactorings y buenos nombres.
como por ejemplo:
public void testMethodBlaBla() {
Person person = anyPerson(); //nos da igual cualquier person
.....
}
public void testMethod() {
Person person = personNamed("Juan"); //solo nos interesa tener un person con nombre juan para este test
....
}
public void testMethod() {
Person personWithDocumentC = personWithDocumentType(Type.C);
...
}
podría pasar también que la creación de Persons sea requerida por diferentes TestCases, por lo que el siguiente paso consistirá sacar estos métodos de a alguna clase utilitaria (básicamente una llena de factoryMethods)
por lo que creamos un Helper para poder remover esta duplicación…
public class PersonFactory {
public static Person anyPerson() {
....
}
public static Person personNamed() {
....
}
.....
}
Cada vez que alguien necesita crear un person particular y no está en los métodos ofrecidos por el Factory, agregara uno nuevo.
Luego por ejemplo en java 5, con imports estáticos podremos lograr el mismo código que antes.
Muchas veces estos factories, empiezan a crecer en cantidad de métodos, ya que no siempre los métodos disponibles satisfacen nuestra necesidad, también empiezan a aparecer métodos que combinan ciertas características, ej: personNamedAndWithTypeDocument(name, type) y empiezan verdaderas explosiones combinatorias…
Una solución posible a esta última problemática (o una vueltita más de rosca) consiste en utilizar builders.
Con builders podremos especificar de una forma declarativa que Person queremos en cada uno de los tests.
Ejemplo:
cualquier persona (podríamos tener un método en el builder que sea anyPerson() para que comunique mas y que internamente retorne build())
Person aPerson = aPerson().build();
una persona con nombre particular (los demás datos defaults)
Person aPerson = aPerson()
.withName("Pedro")
.build();
una persona con datos bien específicos
Person aPerson = aPerson()
.withName("Pedro")
.withSurname("Gonzalez")
.with(aDocument()
.withType(Document.Type.A)
.withNumber(Document.Number.from(20))
.build())
.build();
cualquier persona con documento tipo C
Person aPerson = aPerson() .with(aDocument() .withType(Document.Type.C) .build()) .build();
Como podemos ver se va formando un DSL interno, que nos permitirá no solo combinar como nos convenga en el test, sino también comunicara cual es el la intención de uso del objeto que estamos creando. (Sin ruido)
Si no le indicamos datos necesarios, nos proveerá valores default.
aPerson() y aDocument() son métodos estáticos de los builders PersonBuilder y DocumentBuilder respectivamente que al importarlos estáticamente nos permiten que el código sea más legible.
el método build() termina creando la instancia que le especificamos al builder.
los métodos withXXX nos permiten especificar o no lo que queramos que tenga nuestra instancia, no llevan orden en particular.
El truco para poder encadenar los withXXX simplemente consiste en retornar el mismo builder al final de cada uno.
que les parece?
A continuación el código de los builders…
public class PersonBuilder {
private String name = "no name";
private String surname = "no surname";
private Document document = new Document(Document.Type.A, new Document.Number(1));
public static PersonBuilder aPerson() {
return new PersonBuilder();
}
public Person build() {
Person person = new Person(name, surname, document);
return person;
}
public PersonBuilder withName(final String aName) {
name = aName;
return this;
}
public PersonBuilder withSurname(final String aSurname) {
surname = aSurname;
return this;
}
public PersonBuilder with(final Document aDocument) {
document = aDocument;
return this;
}
}
import com.hexacta.hat.builders.Document.Number;
import com.hexacta.hat.builders.Document.Type;
public class DocumentBuilder {
public static DocumentBuilder aDocument() {
return new DocumentBuilder();
}
private Type type = Type.A;
private Number number = new Number(1);
public DocumentBuilder withType(final Type aType) {
type = aType;
return this;
}
public DocumentBuilder withNumber(final Number aNumber) {
number = aNumber;
return this;
}
public Document build() {
return new Document(type, number);
}
}