Verwenden von HSQLDB + DBUnit zum Unit-Testen von Java-Datenbankcode

Vorwort


Es wird angenommen, dass Unit-Tests keine realen Objekte verwenden sollten (d. H. Datenbankverbindungen, Netzwerk-Sockets und ähnliche Ressourcen). Auf dieser Basis haben sich viele Holivars entwickelt - Sie müssen den Code testen, der mit Datenbanken funktioniert, oder es ist ein schlechter Ton. Wenn es sich um Tests handelt, kann dies als Komponententest oder als Funktionstest (oder Integrationstest, da wir die gemeinsame Arbeit von zwei Softwareumgebungen / Modulen testen) bezeichnet werden. Streitigkeiten und Schlachten hören nicht auf. Ich bitte die Leser, sich nicht von den heiligen Kriegen ablenken zu lassen, sondern dieses Material als Denkanstoß zu akzeptieren. Vergessen wir nicht, dass das von mir beschriebene Werkzeug nur ein Werkzeug ist, dessen Anwendbarkeit von der Aufgabe abhängt.

Werkzeugauswahl


Das vielleicht Schwierigste beim Unit-Testen ist die Überprüfung des Codes, der mit Datenbankverbindungen funktioniert (im Großen und Ganzen die Überprüfung des Codes, der mit externen Objekten funktioniert). Ja, Sie können Mocks anstelle von Verbindungen verwenden. Wenn Sie jedoch mehr als eine Operation mit einem JDBC-Provider ausführen, ist es wahrscheinlicher, dass Sie im Mock-Objekt einen Fehler machen, als dass Sie ihn im Code mithilfe des letzteren abfangen. Was bleibt Die Verwendung echter Datenbanken ist ebenfalls schlecht, da Sie den Datenbankserver nicht in das Repository stellen können ... Und wenn ich sage, dass Sie es sehr gut stellen können und es bereits vorhanden ist? Die Lösung für unser Problem ist HSQLDB .

HSQLDB ist eine relationale Datenbank, die vollständig in Java geschrieben ist. Gleichzeitig kann der Datenbankserver, was sehr bemerkenswert ist, als separate Instanz oder in einer Java-Anwendung erstellt werden. Die geringe Größe und die Fähigkeit, die gesamte Datenbank (standardmäßig) vollständig im Speicher zu speichern, machen HSQLDB zu einem idealen Datenbankserver für Unit-Tests. Da aus Sicht von JDBC und ORM die Implementierung des DBMS keine Rolle spielt (wenn Sie den SQL-Standard einhalten und die DBMS-Modulerweiterungen nicht missbrauchen), können wir die Verbindung zu PostgreSQL oder Oracle während des Unit-Tests problemlos durch die Verbindung zu HSQLDB ersetzen.

Angenommen, wir haben eine Datenbank, die sich vollständig im Arbeitsspeicher befindet und nur minimale Ressourcen verbraucht. Vor der Durchführung von Tests müssen Daten eingegeben werden. Dies sollte mit einer universelleren Methode erfolgen als mit dem Schreiben von SQL-Abfragen. Wir müssen auch den Status der Datenbank nach Operationen darauf überprüfen. Wie Sie sehen, ist die Idee, Daten daraus zu erhalten und sie manuell mit der Referenz zu vergleichen, äußerst schlecht. Um das Problem der Initialisierung und Überprüfung der Ergebnisse des Vorgangs zu lösen, wurde die DBUnit- Bibliothek erstellt , die sich ideal für die Automatisierung der Initialisierung der Datenbank und die anschließende Überprüfung von Datensätzen eignet.

Anwendungsbeispiel


Um die Funktionen von HSQLDB und DBUnit zu demonstrieren, erstellen wir eine Klasse, deren Konstruktor einen Datenbankconnector als Parameter akzeptiert. Die Klasse verfügt über eine Methode, die eine Textzeichenfolge als Parameter verwendet, diese in separate Wörter aufteilt und Statistiken zur Häufigkeit des Auftretens von Wörtern in der Datenbanktabelle hinzufügt. Unsere Klasse wird so aussehen:

public class User {
	private Connection sqlConnection;
	public User(Connection sqlConnectopn) {
		this.sqlConnection = sqlConnectopn;
	}
	private int insertOriginalString(String originalString) throws SQLException {
		int originalStringId = 0;
		PreparedStatement psInsert = sqlConnection.
			prepareStatement(
				"INSERT INTO original_strings (strings, date) VALUES (?, now())",
				PreparedStatement.RETURN_GENERATED_KEYS
			);
		psInsert.setString(1, originalString);
		psInsert.execute();
		ResultSet rsInsert = psInsert.getGeneratedKeys();
		if(rsInsert.next()) {
			originalStringId = rsInsert.getInt(1);
		}
		else {
			throw new RuntimeException();
		}
		rsInsert.close();
		psInsert.close();
		return originalStringId;
	}
	private int insertToken(int originalStringId, String token) throws SQLException {
		int tokenId = 0;
		PreparedStatement psTokenId = sqlConnection.
				prepareStatement("SELECT id FROM tokens WHERE word = ?");
		psTokenId.setString(1, token);
		ResultSet rsToken = psTokenId.executeQuery();
		if(rsToken.next()) {
			tokenId = rsToken.getInt(1);
		}
		else {
			PreparedStatement psInsertToken = sqlConnection.
					prepareStatement(
							"INSERT INTO tokens (word) VALUES (?)", 
							PreparedStatement.RETURN_GENERATED_KEYS
					);
			psInsertToken.setString(1, token);
			psInsertToken.execute();
			ResultSet rsInserToken = psInsertToken.getGeneratedKeys();
			if(rsInserToken.next()) {
				tokenId = rsInserToken.getInt(1);
			}
			else {
				throw new RuntimeException();
			}
			rsInserToken.close();
			psInsertToken.close();
		}
		rsToken.close();
		psTokenId.close();
		return tokenId;
	}
	private void linkTokenToString(int originalStringId, int tokenId) throws SQLException {
		PreparedStatement psCreateLink = sqlConnection.
			prepareStatement("INSERT INTO links (original_string_id, token_id) VALUES(?,?)");
		psCreateLink.setInt(1, originalStringId);
		psCreateLink.setInt(2, tokenId);
		psCreateLink.execute();
	}
	public void logRequestString(String requestString) throws SQLException {
		String preParsed = requestString.replaceAll("\\W+", " ");
		String[] tokens = preParsed.split(" ");
		if(tokens.length > 0) {
			int originalStringId = insertOriginalString(requestString);
			for(String token: tokens) {
				linkTokenToString(
						originalStringId, 
						insertToken(originalStringId, token)
				);
			}
		}
	}
}


Schreiben Sie nun einen Unit-Test dazu.

public class UserTest {
	private IDatabaseTester tester = null;
	@Before public void instantiate() throws Exception {
		//Creating databse server instance
		tester = new JdbcDatabaseTester("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:" + UUID.randomUUID().toString(), "sa", "");
		//Creating tables
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU2 AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU3 AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE TOKENS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU NOT NULL PRIMARY KEY, WORD LONGVARCHAR NOT NULL)").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE ORIGINAL_STRINGS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU2 NOT NULL PRIMARY KEY, STRINGS LONGVARCHAR NOT NULL,DATE TIMESTAMP NOT NULL)").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE LINKS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU3 NOT NULL PRIMARY KEY,TOKEN_ID INT NOT NULL,ORIGINAL_STRING_ID INT NOT NULL)").execute();
		//Setting DATA_FACTORY, so DBUnit will know how to work with specific HSQLDB data types
		tester.getConnection().getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new HsqldbDataTypeFactory());
		//Getting dataset for database initialization 
		IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader().getResourceAsStream("template_set.xml"));
		//Initializing database
		tester.setDataSet(dataSet);
		tester.onSetup();
	}
	@Test
	public void logRequestStringTest() throws SQLException, Exception {
		User man = new User(tester.getConnection().getConnection());
		man.logRequestString("Hello, world!");
		ITable template = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader().
				getResourceAsStream("check_set.xml")).getTable("tokens");
		ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"),
				template.getTableMetaData().getColumns());
		Assertion.assertEquals(template, actual);
	}
}


Dataset-Dateien sehen folgendermaßen aus:

template_set.xml


check_set.xml


Beim Anzeigen eines Komponententests kann sofort die Frage auftauchen: „Warum ist der Code zum Erstellen von Tabellen in der Datenbank im Komponententest enthalten ?! Sie versprachen, Datensätze aus Dateien herunterzuladen? " Ja, das stimmt, wir laden die Sets aus Dateien, aber die Beschreibung der Datenbankstruktur mithilfe von XML und die Kompatibilität mit allen Datenbanktreibern ist aufgrund der unterschiedlichen Syntax von DDL-Abfragen für jedes DBMS kein einfacher Prozess. Daher ist eine solche Funktionalität in DBUnit nicht verfügbar.
Ich möchte Ihre Aufmerksamkeit auf das folgende Design lenken:
ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"),
				template.getTableMetaData().getColumns());

Die Funktion DefaultColumnFilterfiltert die Spalten, um den Datensatz zu vergleichen, ohne die ID der Datensätze zu berücksichtigen.

Fazit


In diesem Artikel habe ich das einfachste Beispiel für die Arbeit mit einer Datenbank analysiert. Der Leser kann sich leicht vorstellen, dass ein solcher Testansatz nicht nur für die "reine" Verwendung von ODBC-Verbindungen, sondern auch für ORM-Frameworks anwendbar ist. Stabiler Code für Sie!

Jetzt auch beliebt: