Artiklar skrivna av
Leo Wentzel

När ett test skrivs för en klass som har beroenden till en datakälla, en extern service eller bara en annan klass är mockning ofta väldigt användbart. Ibland kan detta leda till att produktionskod anpassas för att det ska gå att skriva dessa tester. Nedan är exempel på ett test som testar en service som använder en entity manager.

package se.cygni.blog;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import javax.persistence.EntityManager;

import org.junit.Before;
import org.junit.Test;

public class BlogServiceTest {

    private BlogService service;
    private EntityManager entityManager;

    @Before
    public void setup() {
        entityManager = mock(EntityManager.class);
        service = new BlogService(entityManager);
    }

    @Test
    public void addEntry() {
        Entry entry = new Entry("title", "text");

        service.addEntry(entry);

        verify(entityManager).persist(entry);
    }

}

Testet i exemplet är relativt enkelt och verifierar endast att persist anropas i addEntry och ger oss följande kod för ett Spring-baserat projekt:

package se.cygni.blog;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BlogService {

    @PersistenceContext
    private EntityManager entityManager;

    public BlogService() {
    }

    protected BlogService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public void addEntry(Entry entry) {
        entityManager.persist(entry);
    }

}

För att kunna använda en mockad entityManager har vi lagt till en konstruktor som tar en EntityManager som BlogService sedan kan använda. Använder vi dessutom ”field injection” är vi tvugna att lägga till en no-args-konstruktor. Alternativt hade vi kunnat lägga till en setter istället.

    protected void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

Det är väl antagligen inte så illa att behöva lägga till konstruktorer alternativt setters även om det blir värre när en klass har flera beroenden. Däremot känns det ju lite onödigt när både Spring och Java EE numera stödjer field injection. Det går ju att köra enhetstesterna med till exempel Spring och låta Spring injicera alla beroenden. Detta anses dock inte helt lämpligt för enhetstester utan lämpar sig bättre för integrationstester. Istället kan Mockito ta hand om injicering, genom att köra testerna med MockitoJUnitRunner och använda annotationerna @Mock och @InjectMocks. Mockito fungerar då i princip som en enkel IOC-container och några extra konstruktorer eller setters behövs inte.

package se.cygni.blog;

import static org.mockito.Mockito.verify;

import javax.persistence.EntityManager;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class BlogServiceTest {

    @Mock
    private EntityManager entityManager;

    @InjectMocks
    private BlogService service = new BlogService();

    @Test
    public void addEntry() {
        Entry entry = new Entry("title", "text");

        service.addEntry(entry);

        verify(entityManager).persist(entry);
    }
}

Nyligen läste jag ett intressant blogginlägg där Spring jämfördes med Java EE 6. Författaren drar slutsatsen att det numera är minst lika enkelt att utveckla en applikation för Java EE-plattformen som för Spring. Då Spring, Spring MVC inkluderat, är väldigt populärt blev jag nyfiken på hur detta kan integreras med Java EE för att lättare dra nytta av allt som en Java EE-container kan erbjuda. Det visade sig att detta var förbluffande enkelt, till exempel går det utan ytterligare Spring-konfiguration direkt injicera en Session Bean i en Spring-controller.

@Controller
public class MyController {

    @EJB(mappedName="java:module/MyService")
    private MyService myService;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Model model) {
        model.addAttribute("message", myService.getMessage());
        return "home";
    }
}

Nyckeln till att injektionen med @EJB-annotation fungerar, i exemplet ovan, är att mappedName används. Spring utför då en JNDI-lookup på det angivna JNDI-namnet. Eftersom JNDI har standardiserats i Java EE 6 är dessutom namnen portabla mellan applikationsservrar. Stödet för Java EE 6 kommer säkerligen förbättras i framtida versioner av Spring så det finns bra defaults för ejb-lookups med JNDI.

En annan spännande detalj med detta exempel är att MyService inte behöver vara ett interface eftersom Java EE 6 inte kräver detta för lokala session beans. Dessutom kan session beans paketeras tillsammans med resten av webbapplikationen i war-arkivet. D.v.s. det behövs ingen ejb-jar och inget EAR-arkiv.

Läs hela artikeln på Java Code Geeks.

Java Bean Validation (JSR 303) definierar en meta-data-modell och ett API för validering av klasser. Implementationer av denna specifikation gör det möjligt att definiera och utföra validering med hjälp av annotationer eller xml. Exemplet nedan illusterar detta för ett användarnamn:

@NotNull
@Size(min = 5, max = 20)
@Pattern(regexp = "[a-zA-Z0-9\\._-]+")
private String userName;

Bean Validation är inte knutet till något speciellt ramverk eller lager. Det finns däremot bra stöd för Bean Validation i flertalet populära ramverk, såsom Hibernate och JSF 2. Dessutom är det enkelt att utföra validering via API:t:

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
...
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Customer>> constraintViolations = validator.validate(customer);

För att komma igång med ett Bean Validation (Hibernate Validator 4.x är referensimplementationen) i ett Maven-projekt lägg till följande beroenden i din pom:

<dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-validator</artifactId>
 <version>4.2.0.Final</version>
</dependency>
<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-jdk14</artifactId>
 <version>1.6.1</version>
</dependency>

Används Hibernate eller JSF 2 räcker det oftast med att lägga till ovanstående beroenden så länge de finns tillgängliga i applikationens class path. Används JSF 2 går det till exempel att aktivera validering för ett inmatningsfält på följande sätt:

<h:inputText value="#{customer.age}">
  <f:validateBean/>
</h:inputText>

Bean validation stödjer dessutom validering av grupper som gör det möjligt att validera olika aspekter av ett objekt:

@Min(value=15, groups=AgeCheck.class)
private int age;

Logback, ett ramverk för loggning på Java-plattformen, släpptes nyligen som version 1.0. Initiativtagare är personen bakom log4j och SLF4J, Ceki Gülcü. Logback bygger på log4j och kan därför ses som en direkt uppföljare. Logback kommer med inbyggt stöd för SLF4J för att underlätta byte av implementation av ramverk. Några av fördelarna över log4j som sägs vara högre prestanda, utökad testsvit för bättre tillförlitlighet, förbättrad automatisk omladdning av konfigurationsfiler, mer avancerade filter och integration med servlet-containers för loggning av http-access.

För att testa Logback, lägg till följande maven-beroende:

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.0.0</version>
</dependency>

Logback-classic beror på logback-core och SLF4J och därför kommer dessa automatiskt inkluderas i projektet. Därefter är det bara att logga på samma sätt som med SLF4J:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
Logger logger = LoggerFactory.getLogger(App.class);
logger.info("Hello World!");

Konfiguration påminner om log4j men skiljer sig på vissa punkter. Nedan är ett enkelt exempel på en Logback-konfiguration:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="3 seconds">

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logFile.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logFile.%d{yyyy-MM-dd_HH-mm}.log.zip</fileNamePattern>
            <maxHistory>5</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>%-26(%d{HH:mm:ss,SSS} [%thread]) %-5level %logger{32} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>

</configuration>

Det är vanligt förekommande att utvecklare behöver skriva kod för att konvertera listor. Listor kan behöva filtreras eller konverteras mellan olika format. Ett vanligt förekommande mönster när en lista ska konverteras är att skapa en ny lista för resultatet, iterera över ett givet data-set, konvertera varje post och lägga det till resultat-listan. Det är inte helt ovanligt att resultatet också filtreras i denna process. Nedan är ett exempel i Java:

Collection<String> c = new ArrayList<String>();
for (Person person : persons) {
  if (person.getAge() >= 18) {
    c.add(person.getName());
  }
}

Ovanstående exempel kan tyckas både enkelt och funktionellt men jämför med hur detta kan lösas i Python:

l = [person.name for person in persons if person.age >= 18]

Alternativt genom att använda filter och lambda kan det också skrivas så här:

l = [person.name for person in filter(lambda person: person.gender >= 18, persons)]

I Java finns tyvärr inte motsvarande funktionalitet. Det finns däremot alternativ för att lösa det på ett liknande sätt. Guava är ett projekt och innehåller flera bibliotek som Google använder till sina Java-projekt. Bland annat erbjuder dessa bibliotek funktioner för att filtrera och transformera listor på ett mer ‘funktionellt’ sätt. Nedan är en lösning som löser samma problem som ovan implementerad med hjälp av dessa funktioner:

Function<Person, String> f = new Function<Person, String>() {
  @Override
  public String apply(Person input) {
    return input.getName();
  }
};

Predicate<Person> p = new Predicate<Person>() {
  @Override
  public boolean apply(Person input) {
    return input.getAge() >= 18;
  }
};

Collection<String> c = Collections2.transform(Collections2.filter(persons, p), f);

Enklare exempel som detta blir möjligtvis varken mer kompakta eller lättlästa. När konverteringar som denna däremot upprepas så blir detta alternativ genast mer attraktivt. Jämför till exempel med JDBC-mappers som inte helt sällan återanvänds. Guava är licensierat under Apache License 2.0 och innehåller flertalet användbara funktioner. Ett alternativ är också Apache Commons, värt att nämna är att Apache Commons för närvarande tyvärr inte stödjer Java generics.

Lägg till följande beroende i din Maven-pom för att testa Guava:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>10.0.1</version>
</dependency>

I väntan på en Java-version (Java 8?) som stödjer closures och lambda-funktioner går det också att använda lambdaj. Med lambdaj kan en lista till exempel konverteras och filtreras på följande sätt:

import static ch.lambdaj.Lambda.*;
import static org.hamcrest.Matchers.*;
...
Collection<String> c = collect(filter(having(on(Person.class).getAge(), greaterThanOrEqualTo(18)), persons), on(Person.class).getName());

Lägg till följande Maven-beroende för att använda lambdaj:

<dependency>
  <groupId>com.googlecode.lambdaj</groupId>
  <artifactId>lambdaj</artifactId>
  <version>2.3.1</version>
</dependency>

JRebel är ett trevligt utvecklingsverktyg som gör det möjligt att omedelbart testa ändringar i en applikation som körs utan att starta om applikationsservern (ex Tomcat, Jetty, Glassfish, JBoss, WebSphere etc). Denna JVM-plugin stödjer omladdning av allt från JPA-entiteter, Session beans, JSP:er till resurs- och konfigurationsfiler. Förändringar i klasshierarkier stöds dock inte för tillfället. Det finns plugins till de populäraste utvecklingsmiljöerna och flertalet Java-ramverk stöds.

JRebel kostar en slant men kan spara tid och effektivisera utvecklingsarbetet, framförallt när det gäller större och tyngre applikationer som tar lång tid att bygga och/eller deploya.

Läs mer om JRebel på http://www.zeroturnaround.com/jrebel/