Java >> Java Tutorial >  >> Tag >> Spring

Spring From the Trenches:Verwenden von Nullwerten in DbUnit-Datensätzen

Wenn wir Integrationstests für eine Anwendung schreiben, die Spring Framework verwendet, können wir DbUnit mithilfe von Spring Test DbUnit in das Spring-Testframework integrieren.

Allerdings ist diese Integration nicht problemlos .

Oft müssen wir null einfügen Werte in die Datenbank, bevor unsere Tests ausgeführt werden, oder überprüfen Sie, ob der in der spezifischen Tabellenspalte gespeicherte Wert null ist . Dies sind sehr einfache Anwendungsfälle, aber es ist schwierig, Integrationstests zu schreiben, die sie unterstützen.

Dieser Blogbeitrag identifiziert die Probleme im Zusammenhang mit null Werte und beschreibt, wie wir sie lösen können. Beginnen wir mit einem kurzen Blick auf das zu testende System.

Das getestete System

Die getestete „Anwendung“ hat eine Entität und ein Spring Data JPA-Repository, das CRUD-Operationen für diese Entität bereitstellt.

Unsere Entitätsklasse heißt Todo und der relevante Teil seines Quellcodes sieht wie folgt aus:

import javax.persistence.*;

@Entity
@Table(name="todos")
public class Todo {

    private static final int MAX_LENGTH_DESCRIPTION = 500;
    private static final int MAX_LENGTH_TITLE = 100;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "description", nullable = true, length = MAX_LENGTH_DESCRIPTION)
    private String description;

    @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE)
    private String title;

    @Version
    private long version;
	
	//Constructors, builder class, and getters are omitted.
}

Unsere Spring Data JPA-Repository-Schnittstelle heißt TodoRepository , und es erweitert das CrudRepository Schnittstelle. Dieses Repository stellt CRUD-Operationen für Todo bereit Objekte. Es deklariert auch eine Abfragemethode, die alle Aufgabeneinträge zurückgibt, deren Beschreibung mit dem angegebenen Suchbegriff übereinstimmt.

Der Quellcode des TodoRepository Die Benutzeroberfläche sieht wie folgt aus:

import org.springframework.data.repository.CrudRepository;

public interface TodoRepository extends CrudRepository<Todo, Long> {

    List<Todo> findByDescription(String description);
}

Lassen Sie uns weitermachen und herausfinden, wie wir mit null umgehen können Werte, wenn wir Integrationstests für Code schreiben, der entweder Informationen aus einer relationalen Datenbank liest oder dort Informationen speichert.

Umgang mit Nullwerten

Wenn wir Integrationstests für unseren Datenzugriffscode schreiben, müssen wir die Datenbank vor jedem Testfall in einen bekannten Zustand initialisieren und sicherstellen, dass die richtigen Daten in die Datenbank geschrieben werden.

Dieser Abschnitt identifiziert die Probleme, mit denen wir konfrontiert sind, wenn wir Integrationstests schreiben, die

  • Verwenden Sie flache XML-Datensätze.
  • Schreiben Sie null Werte in die Datenbank oder stellen Sie sicher, dass der Wert einer Tabellenspalte null ist .

Wir werden auch lernen, wie wir diese Probleme lösen können.

Einfügen von Nullwerten in die Datenbank

Wenn wir Integrationstests schreiben, die Informationen aus der Datenbank lesen, müssen wir diese Datenbank in einen bekannten Zustand initialisieren, bevor unsere Tests aufgerufen werden, und manchmal müssen wir null einfügen Werte in die Datenbank.

Da wir flache XML-Datensätze verwenden, können wir null einfügen Wert zu einer Tabellenspalte hinzufügen, indem der entsprechende Attributwert weggelassen wird. Das heißt, wenn wir null einfügen wollen Wert in die Beschreibung Spalte der Aufgaben Tabelle können wir dies tun, indem wir den folgenden DbUnit-Datensatz verwenden:

<dataset>
    <todos id="1" title="FooBar" version="0"/>
</dataset>

Oft müssen wir jedoch mehr als eine Zeile in die verwendete Datenbanktabelle einfügen. Der folgende DbUnit-Datensatz (todo-entries.xml ) fügt zwei Zeilen in die Aufgaben ein Tabelle:

<dataset>
    <todos id="1" title="FooBar" version="0"/>
    <todos id="2" description="description" title="title" version="0"/>
</dataset>

Lassen Sie uns herausfinden, was passiert, wenn wir einen Integrationstest in findByDescription() schreiben Methode des TodoRepository Schnittstelle und initialisieren Sie unsere Datenbank mit dem vorherigen Datensatz (todo-entries.xml ). Der Quellcode unseres Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
public class ITTodoRepositoryTest {

    private static final Long ID = 2L;
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
    private static final long VERSION = 0L;

    @Autowired
    private TodoRepository repository;

	@Test
	@DatabaseSetup("todo-entries.xml")
	public void findByDescription_ShouldReturnOneTodoEntry() {
		List<Todo> todoEntries = repository.findByDescription(DESCRIPTION);
		assertThat(todoEntries).hasSize(1);

		Todo found = todoEntries.get(0);
		assertThat(found.getId()).isEqualTo(ID);
		assertThat(found.getTitle()).isEqualTo(TITLE);
		assertThat(found.getDescription()).isEqualTo(DESCRIPTION);
		assertThat(found.getVersion()).isEqualTo(VERSION);
	}
}

Wenn wir diesen Integrationstest ausführen, erhalten wir den folgenden Behauptungsfehler:

java.lang.AssertionError: 
Expected size:<1> but was:<0> in: <[]>

Das bedeutet, dass der richtige Aufgabeneintrag nicht in der Datenbank gefunden wurde. Was ist passiert? Unsere Abfragemethode ist so einfach, dass sie funktionieren sollte, insbesondere da wir die richtigen Daten in die Datenbank eingefügt haben, bevor unser Testfall aufgerufen wurde.

Naja, eigentlich die Beschreibung Spalten beider Zeilen sind null. Die DbUnit-FAQ beschreibt, warum dies passiert ist:

DbUnit verwendet das erste Tag für eine Tabelle, um die zu füllenden Spalten zu definieren. Wenn die folgenden Datensätze für diese Tabelle zusätzliche Spalten enthalten, werden diese daher nicht ausgefüllt.

Es bietet auch eine Lösung für dieses Problem:

Seit DBUnit 2.3.0 gibt es eine Funktion namens "Column Sensing", die grundsätzlich das gesamte XML in einen Puffer einliest und dynamisch neue Spalten hinzufügt, wenn sie erscheinen.

Wir können die Spaltenerkennung aktivieren, indem Sie diesen Schritten folgen:

  1. Erstellen Sie eine Dataset-Loader-Klasse, die den AbstractDataSetLoader erweitert Klasse.
  2. Überschreiben Sie das geschützte IDateSet createDataSet(Resource resource) Methode des AbstractDataSetLoader Klasse.
  3. Implementieren Sie diese Methode, indem Sie die Spaltenerkennung aktivieren und ein neues FlatXmlDataSet zurückgeben Objekt.

Der Quellcode des ColumnSensingFlatXmlDataSetLoader Klasse sieht wie folgt aus:

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;
import java.io.InputStream;

public class ColumnSensingFlatXMLDataSetLoader extends AbstractDataSetLoader {
	@Override
	protected IDataSet createDataSet(Resource resource) throws Exception {
		FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
		builder.setColumnSensing(true);
		try (InputStream inputStream = resource.getInputStream()) {
			return builder.build(inputStream);
		}
	}
}

Wir können unsere Testklasse jetzt so konfigurieren, dass sie diesen Data-Et-Loader verwendet, indem wir unsere Testklasse mit @DbUnitConfiguration kommentieren -Anmerkung und Festlegen des Werts ihres Ladeprogramms -Attribut zu ColumnSensingFlatXmlDataSetLoader.class .

Der Quellcode unseres festen Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITTodoRepositoryTest {

    private static final Long ID = 2L;
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
    private static final long VERSION = 0L;

    @Autowired
    private TodoRepository repository;

	@Test
	@DatabaseSetup("todo-entries.xml")
	public void findByDescription_ShouldReturnOneTodoEntry() {
		List<Todo> todoEntries = repository.findByDescription(DESCRIPTION);
		assertThat(todoEntries).hasSize(1);

		Todo found = todoEntries.get(0);
		assertThat(found.getId()).isEqualTo(ID);
		assertThat(found.getTitle()).isEqualTo(TITLE);
		assertThat(found.getDescription()).isEqualTo(DESCRIPTION);
		assertThat(found.getVersion()).isEqualTo(VERSION);
	}
}

Wenn wir unseren Integrationstest zum zweiten Mal ausführen, besteht er.

Lassen Sie uns herausfinden, wie wir diese Null überprüfen können Werte werden in der Datenbank gespeichert.

Verifizieren, dass der Wert einer Tabellenspalte Null ist

Wenn wir Integrationstests schreiben, die Informationen in der Datenbank speichern, müssen wir sicherstellen, dass wirklich die richtigen Informationen in der Datenbank gespeichert werden, und manchmal müssen wir überprüfen, ob der Wert einer Tabellenspalte null ist .

Wenn wir beispielsweise einen Integrationstest schreiben, der überprüft, ob die richtigen Informationen in der Datenbank gespeichert werden, wenn wir einen Aufgabeneintrag erstellen, der keine Beschreibung hat, müssen wir sicherstellen, dass ein null Wert wird in die Beschreibung eingefügt Spalte der Aufgaben Tabelle.

Der Quellcode unseres Integrationstests sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITTodoRepositoryTest {

    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";

    @Autowired
    private TodoRepository repository;

    @Test
    @DatabaseSetup("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-without-description-expected.xml")
    public void save_WithoutDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(null)
                .build();

        repository.save(todoEntry);
    }
}
Dies ist kein guter Integrationstest, da er nur testet, ob Spring Data JPA und Hibernate ordnungsgemäß funktionieren. Wir sollten unsere Zeit nicht damit verschwenden, Tests für Frameworks zu schreiben. Wenn wir einem Framework nicht vertrauen, sollten wir es nicht verwenden.

Wenn Sie lernen möchten, gute Integrationstests für Ihren Datenzugriffscode zu schreiben, sollten Sie mein Tutorial mit dem Titel lesen:Tests für Datenzugriffscode schreiben.

Der DbUnit-Datensatz (no-todo-entries.xml ), die zum Initialisieren unserer Datenbank verwendet wird, sieht wie folgt aus:

<dataset>
    <todos/>
</dataset>

Da wir die Beschreibung des gespeicherten Aufgabeneintrags nicht festlegen, die Beschreibung Spalte der Aufgaben Tabelle sollte null sein . Das bedeutet, dass wir es aus dem Datensatz weglassen sollten, der überprüft, ob die richtigen Informationen in der Datenbank gespeichert werden.

Dieser Datensatz (save-todo-entry-with-description-expected.xml ) sieht folgendermaßen aus:

<dataset>
    <todos id="1" title="title" version="0"/>
</dataset>

Wenn wir unseren Integrationstest ausführen, schlägt er fehl und wir sehen die folgende Fehlermeldung:

junit.framework.ComparisonFailure: column count (table=todos, expectedColCount=3, actualColCount=4) 
Expected :[id, title, version]
Actual   :[DESCRIPTION, ID, TITLE, VERSION]

Das Problem ist, dass DbUnit erwartet, dass die todos Tabelle hat nur id , Titel und Version Säulen. Der Grund dafür ist, dass diese Spalten die einzigen Spalten sind, die aus der ersten (und einzigen) Zeile unseres Datensatzes gefunden werden.

Wir können dieses Problem lösen, indem wir ein ReplacementDataSet verwenden . Ein ReplacementDataSet ist ein Dekorator, der die aus einer flachen XML-Datensatzdatei gefundenen Platzhalter durch die Ersatzobjekte ersetzt. Lassen Sie uns unsere benutzerdefinierte Dataset-Loader-Klasse ändern, um ein ReplacementDataSet zurückzugeben Objekt, das '[null]'-Strings durch null ersetzt .

Wir können dies tun, indem wir die folgenden Änderungen an unserem benutzerdefinierten Dataset-Loader vornehmen:

  1. Fügen Sie ein privates createReplacementDataSet() hinzu -Methode zur Dataset-Loader-Klasse. Diese Methode gibt ein ReplacementDataSet zurück Objekt und nimmt ein FlatXmlDataSet Objekt als Methodenparameter.
  2. Implementieren Sie diese Methode, indem Sie ein neues ReplacementDataSet erstellen Objekt und Rückgabe des erstellten Objekts.
  3. Ändern Sie das createDataSet() -Methode zum Aufrufen des privaten createReplacementDataSet() -Methode und geben das erstellte ReplacementDataSet zurück Objekt.

Der Quellcode des ColumnSensingReplacementDataSetLoader Klasse sieht wie folgt aus:

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;

import java.io.InputStream;

public class ColumnSensingReplacementDataSetLoader extends AbstractDataSetLoader {

    @Override
    protected IDataSet createDataSet(Resource resource) throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        builder.setColumnSensing(true);
        try (InputStream inputStream = resource.getInputStream()) {
            return createReplacementDataSet(builder.build(inputStream));
        }
    }

    private ReplacementDataSet createReplacementDataSet(FlatXmlDataSet dataSet) {
        ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet);
		
		//Configure the replacement dataset to replace '[null]' strings with null.
        replacementDataSet.addReplacementObject("[null]", null);
        
		return replacementDataSet;
    }
}

Wir können unseren Integrationstest beheben, indem Sie diesen Schritten folgen:

  1. Konfigurieren Sie unsere Testklasse, um die verwendeten DbUnit-Datensätze zu laden, indem Sie den ColumnSensingReplacementDataSetLoader verwenden Klasse.
  2. Ändern Sie unseren Datensatz, um zu überprüfen, ob der Wert der Beschreibung Spalte ist null .

Zuerst , müssen wir unsere Testklasse konfigurieren, um die DbUnit-Datensätze mithilfe des ColumnSensingReplacementDataSetLoader zu laden Klasse. Denn wir haben unsere Testklasse bereits mit der @DbUnitConfiguration annotiert , müssen wir den Wert seines Loaders ändern -Attribut zu ColumnSensingReplacementDataSetLoader.class .

Der Quellcode der festen Testklasse sieht wie folgt aus:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class)
public class ITTodoRepositoryTest {

    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";

    @Autowired
    private TodoRepository repository;

    @Test
    @DatabaseSetup("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-without-description-expected.xml")
    public void save_WithoutDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(null)
                .build();

        repository.save(todoEntry);
    }
}

Zweiter , müssen wir überprüfen, ob eine null Wert wird in der Beschreibung gespeichert Spalte der Aufgaben Tisch. Wir können dies tun, indem wir eine Beschreibung hinzufügen Attribut zu den einzigen todos Element unseres Datensatzes und setzt den Wert der Beschreibung Attribut auf '[null]'.

Unser fester Datensatz (save-todo-entry-within-description-expected.xml ) sieht folgendermaßen aus:

<dataset>
    <todos id="1" description="[null]" title="title" version="0"/>
</dataset>

Wenn wir unseren Integrationstest ausführen, wird er bestanden.

Fahren wir fort und fassen zusammen, was wir aus diesem Blogpost gelernt haben.

Zusammenfassung

Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  • DbUnit geht davon aus, dass eine Datenbanktabelle nur die Spalten enthält, die vom ersten Tag gefunden werden, das die Spalten einer Tabellenzeile angibt. Wenn wir dieses Verhalten überschreiben wollen, müssen wir die Spaltenerkennungsfunktion von DbUnit aktivieren.
  • Wenn wir sicherstellen wollen, dass die a null Wert in der Datenbank gespeichert wird, müssen wir Ersatzdatensätze verwenden.
  • Wir haben gelernt, wie wir einen benutzerdefinierten Dataset-Loader erstellen können, der Ersatz-Datasets erstellt und Spaltenerkennung verwendet.
  • Wir haben gelernt, wie wir den Datensatzlader konfigurieren können, der zum Laden unserer DbUnit-Datensätze verwendet wird.

Java-Tag