You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There are three reasons for this proposed enhancement.
First, it's well known that the tested code should not also be used for test verification. If this is done it is possible to write code that is self-consistent but not interoperable with any other implementation. The risk of this happening is much lower if different parties implement the library used to verify the test results, esp. if that library is used by multiple parties.
(Yes, this has happened to me once.)
Second, it's easy for a trivial error to cause database tests to blow chunks. The sheer number of failures can make it difficult to identify the cause. This is why the test frameworks include assume() methods in addition to assert() methods. The former are used to check preconditions and will disable, not fail, a test if they're not satisfied. The same verification methods can be used in both assume() and assert() calls - but unless the development team is disciplined this can easily result in unreadable code.
Finally it's a small conceptual leap from a method called to verify preconditions to a method called to create those preconditions if required.
Code Contribution
I hope to make a PR with a proof-of-concept implementation in the next few days. It's always easier to discuss enhancements when you have a working prototype even if it's far from being ready to merge.
Restricted Scope
It goes without saying that this could become an endless pit - but it's not inevitable if we remember to remain focused on the fact that these methods would only be used to check pre- and post-conditions, and later to establish pre-conditions and possibly to revert the test's changes.
Database Hamcrest Matchers
With this in mind we can identify a handful of custom Hamcrest matchers that can be used by all relational databases that support the standard JDBC API.
Usage
This example is a little handwavy - I've implemented this functionality in the past but will reimplement the
functionality based on my understanding of Hamcrest TypeSafeMatcher<> etc. instead of referring to any
prior code. I'll also be applying a deeper understanding of functional interfaces.
publicclassCrudTest {
privatestaticfinalStringTEST_TABLE_NAME = "test";
privatestaticfinalStringTEST_QUERY = "select id from test";
privatestaticfinalintTEST_RECORD_ID = 10;
@ResourceJdbcDatabaseContainerdb;
@TestpublicvoidtestCreateTable() throwsSQLException {
assumeThat(not(db.tableExists(TEST_TABLE_NAME)));
// call method that creates a tableassertThat(db.tableExists(TEST_TABLE_NAME));
}
Predicate<ResultSet> idExists(ResultSetrs, Object...) throwsSQLException {
returnrs.getInt("id").equals((Integer) id);
}
@TestpublicvoidtestCreateRecord() throwsSQLException {
assumeThat(not(db.recordExists(TEST_QUERY, this::recordIdExists, TEST_RECORD_ID)));
// call method that creates a recordassertThat(db.recordExists(TEST_QUERY, this::recordIdExists, TEST_RECORD_ID));
}
}
Implementation
The initial custom Hamcrest matchers will look something like this
publicclassTableMatcherextendsTypeSafeMatcher<JdbcDatabaseContainer> {
publicTableMatcher(StringtableName) { ... }
@OverridepublicvoiddescribeMismatchSafely(JdbcDatabaseContainerdb, Descriptiondescription) { ... }
@OverridepublicvoiddescribeTo(Descriptiondescription) { ... }
@OverridepublicbooleanmatchesSafely (JdbcDatabaseContainerdb) { ... }
// these are the methods available to the test frameworkpublicstaticMatcher<JdbcDatabaseContainer> tableExists(StringtableName) { ... }
// this allows us to verify the table has the expected schemapublicstaticMatcher<JdbcDatabaseContainer> tableMatches(StringtableName, Predicate<ResultSet> predicate, Object... args) { ... }
}
and
publicclassRecordMatcherextendsTypeSafeMatcher<JdbcDatabaseContainer> {
publicRecordMatcher(StringtableName) { ... }
@OverridepublicvoiddescribeMismatchSafely(JdbcDatabaseContainerdb, Descriptiondescription) { ... }
@OverridepublicvoiddescribeTo(Descriptiondescription) { ... }
@OverridepublicbooleanmatchesSafely (JdbcDatabaseContainerdb) { ... }
// these are the methods available to the test framework// do ANY records exist?publicstaticMatcher<JdbcDatabaseContainer> recordExists(Stringquery);
// does exactly one record exist?publicstaticMatcher<JdbcDatabaseContainer> uniqueRecordExists(Stringquery);
// do ANY records match? Intended to check anything// public static Matcher<JdbcDatabaseContainer> recordMatches(String query, Predicate<ResultSet> predicate, Object... args) { ... }// does exactly one record match? Intended to check anything// public static Matcher<JdbcDatabaseContainer> uniqueRecordMatches(String query, Predicate<ResultSet> predicate, Object... args) { ... }
}
There are a few open questions, e.g.,
whether to use separate catalog, schema, and table name parameters or to use the DatabaseMetaData to properly parse a single string
whether to support positional parameters in the RecordMatcher queries. Should we explicitly expose PreparedStatements in some way?
whether to require the predicates be 'curried' in order to eliminate the Object... args parameter.
The benefit of curried predicates is that we can then add methods that allow callbacks, e.g., it's not hard to imagine an enhancement where the method doesn't just match tables or records - it active involves a callback for arbitrary code to be executed on the matching records. This could be incorpated in the Predicate, of course, but that blurs responsibilities. In ths specific case we could probably add a Consumer<> before the Predicate<> but we again face the question of how we could provide additional information to that consumer, if desired. Fortunately we can punt this question if our initial approach does not include the Object... args parameter.
The actual implementation is straightforward for any JDBC database. The TableMatcher only requires the DatabaseMetaData information. The RecordMatcher will only requires that information plus SELECT privileges.
JdbcDatabaseContainer Modifications
The modifications to JdbcDatabaseContainer are modest.
Semi-related - the proposed LDAP module will also include custom Hamcrest matchers. This will be a much deeper set of matchers since we know the main use of LDAP involves user account management and everyone uses a standard set of schemas to hold the information.
Module
Core
Proposal
There are three reasons for this proposed enhancement.
First, it's well known that the tested code should not also be used for test verification. If this is done it is possible to write code that is self-consistent but not interoperable with any other implementation. The risk of this happening is much lower if different parties implement the library used to verify the test results, esp. if that library is used by multiple parties.
(Yes, this has happened to me once.)
Second, it's easy for a trivial error to cause database tests to blow chunks. The sheer number of failures can make it difficult to identify the cause. This is why the test frameworks include
assume()
methods in addition toassert()
methods. The former are used to check preconditions and will disable, not fail, a test if they're not satisfied. The same verification methods can be used in bothassume()
andassert()
calls - but unless the development team is disciplined this can easily result in unreadable code.Finally it's a small conceptual leap from a method called to verify preconditions to a method called to create those preconditions if required.
Code Contribution
I hope to make a PR with a proof-of-concept implementation in the next few days. It's always easier to discuss enhancements when you have a working prototype even if it's far from being ready to merge.
Restricted Scope
It goes without saying that this could become an endless pit - but it's not inevitable if we remember to remain focused on the fact that these methods would only be used to check pre- and post-conditions, and later to establish pre-conditions and possibly to revert the test's changes.
Database Hamcrest Matchers
With this in mind we can identify a handful of custom Hamcrest matchers that can be used by all relational databases that support the standard JDBC API.
Usage
This example is a little handwavy - I've implemented this functionality in the past but will reimplement the
functionality based on my understanding of Hamcrest
TypeSafeMatcher<>
etc. instead of referring to anyprior code. I'll also be applying a deeper understanding of functional interfaces.
Implementation
The initial custom Hamcrest matchers will look something like this
and
There are a few open questions, e.g.,
DatabaseMetaData
to properly parse a single stringRecordMatcher
queries. Should we explicitly exposePreparedStatements
in some way?Object... args
parameter.The benefit of curried predicates is that we can then add methods that allow callbacks, e.g., it's not hard to imagine an enhancement where the method doesn't just match tables or records - it active involves a callback for arbitrary code to be executed on the matching records. This could be incorpated in the Predicate, of course, but that blurs responsibilities. In ths specific case we could probably add a
Consumer<>
before thePredicate<>
but we again face the question of how we could provide additional information to that consumer, if desired. Fortunately we can punt this question if our initial approach does not include theObject... args
parameter.The actual implementation is straightforward for any JDBC database. The
TableMatcher
only requires theDatabaseMetaData
information. TheRecordMatcher
will only requires that information plusSELECT
privileges.JdbcDatabaseContainer Modifications
The modifications to
JdbcDatabaseContainer
are modest.Needless to say individual modules could overwrite these methods or add their own.
The text was updated successfully, but these errors were encountered: