diff --git a/README.adoc b/README.adoc index 03633012..f574121f 100644 --- a/README.adoc +++ b/README.adoc @@ -188,6 +188,25 @@ For "named" column formats, simply omit the element to emulate NULL values. Alte ---- +### Typed values + +If your code is aware of the types (e.g. by calling getInt on the ResultSet) then it is sufficient to read Strings and the MockResultSet will do the coercion. However, if your code ends up using getObject like some of Spring's methods, then it will always return a String when the real driver would return the actual type. +Type can be encoded to mock how the driver returns: +[source,xml] +---- + + nameage + Erich Eichinger10 + Matthias Bernlöhr20 + +---- + +More types can be added by modifying or extending the XmlTypeRegistry. + +## Recording in spy mode +It is possible to record the jdbc traffic when in spy mode. This will depend on your specific setup. A default version is provided using WireMock mapping json files. +see link:src/test/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorderTest.java[] +To enable without any changes, you can set the recorder field on the JdbcServiceVirtualizationFactory to the provided implementation. Likely you will wish to extend this class for your own case. ## Getting the Binaries @@ -210,7 +229,7 @@ For bugs, feature requests or questions and discussions please use GitHub issues ### Building -To build the project simply run +Works on JDK1.8. To build the project simply run mvn clean install diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java index 38b46d6d..adc9a7d1 100644 --- a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java +++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java @@ -5,6 +5,7 @@ import com.mockrunner.jdbc.StatementResultSetHandler; import com.mockrunner.mock.jdbc.MockConnection; import com.mockrunner.mock.jdbc.MockDataSource; +import com.mockrunner.mock.jdbc.MockResultSet; import com.mockrunner.mock.jdbc.MockStatement; import com.p6spy.engine.common.ConnectionInformation; import com.p6spy.engine.logging.P6LogOptions; @@ -15,6 +16,9 @@ import com.p6spy.engine.spy.P6Factory; import com.p6spy.engine.spy.P6LoadableOptions; import com.p6spy.engine.spy.option.P6OptionsRepository; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.SneakyThrows; import org.apache.http.Header; import org.apache.http.client.methods.CloseableHttpResponse; @@ -25,14 +29,20 @@ import org.apache.http.util.EntityUtils; import javax.sql.DataSource; - +import javax.sql.rowset.CachedRowSet; +import javax.sql.rowset.RowSetProvider; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Stream; @@ -44,15 +54,15 @@ */ public class JdbcServiceVirtualizationFactory implements P6Factory { + @Getter @Setter private String targetUrl; - public String getTargetUrl() { - return targetUrl; - } + @Setter + private WireMockMappingJsonRecorder recorder; + + @Setter + private MockResultSetHelper mockResultSetHelper = new MockResultSetHelper(); - public void setTargetUrl(String targetUrl) { - this.targetUrl = targetUrl; - } public DataSource spyOnDataSource(DataSource ds) { return interceptDataSource(ds); @@ -135,7 +145,7 @@ public synchronized void addStatement(MockStatement statement) { } @Override - public Connection getConnection(Connection conn) throws SQLException { + public Connection getConnection(Connection conn) { return interceptConnection(conn); } @@ -168,62 +178,82 @@ protected Connection interceptConnection(Connection conn) { protected Object interceptPreparedStatementExecution(PreparedStatementInformation preparedStatementInformation, Object underlying, Method method, Object[] args) { CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpPost httpPost = prepareHttpCall(preparedStatementInformation); + + try (CloseableHttpResponse response = httpclient.execute(httpPost)) { + if (response.getStatusLine().getStatusCode() == 200) { + return getResultFromHttpResponse(response, method.getReturnType()); + } + if (response.getStatusLine().getStatusCode() == 400) { + throw getExceptionFromResponse(response); + } + } + + return callUnderlyingMethod(underlying, method, args, preparedStatementInformation); + } + + protected HttpPost prepareHttpCall(PreparedStatementInformation preparedStatementInformation) { final String sql = preparedStatementInformation.getSql(); HttpPost httpPost = new HttpPost(targetUrl); for (Map.Entry e : preparedStatementInformation.getParameterValues().entrySet()) { httpPost.setHeader(e.getKey().toString(), Objects.toString(e.getValue())); } httpPost.setEntity(new StringEntity(sql, "utf-8")); + return httpPost; + } - try (CloseableHttpResponse response = httpclient.execute(httpPost)) { - if (response.getStatusLine().getStatusCode() == 200) { - String responseContent = EntityUtils.toString(response.getEntity(), "utf-8"); - if (int[].class.equals(method.getReturnType())) { - return parseBatchUpdateRowsAffected(responseContent); - } - if (int.class.equals(method.getReturnType())) { - return Integer.parseInt(responseContent); - } - return MockResultSetHelper.parseResultSetFromSybaseXmlString("x", responseContent); - } - if (response.getStatusLine().getStatusCode() == 400) { - final Header reasonHeader = response.getFirstHeader("reason"); - if (reasonHeader == null) throw new AssertionError("missing 'reason' response header"); - final String sqlState = response.getFirstHeader("sqlstate") != null ? response.getFirstHeader("sqlstate").getValue() : null; - final int vendorCode = response.getFirstHeader("vendorcode") != null ? Integer.parseInt(response.getFirstHeader("vendorcode").getValue()) : 0; - throw new SQLException(reasonHeader.getValue(), sqlState, vendorCode); - } + @SneakyThrows + protected Object getResultFromHttpResponse(CloseableHttpResponse response, Class returnType) { + String responseContent = EntityUtils.toString(response.getEntity(), "utf-8"); + if (int[].class.equals(returnType)) { + return parseBatchUpdateRowsAffected(responseContent); } + if (int.class.equals(returnType)) { + return Integer.parseInt(responseContent); + } + return parseResultSetFromResponseContent(responseContent); + } - final Object result = method.invoke(underlying, args); - return result; + protected MockResultSet parseResultSetFromResponseContent(String responseContent) { + return mockResultSetHelper.parseResultSetFromSybaseXmlString("x", responseContent); } + protected Throwable getExceptionFromResponse(CloseableHttpResponse response) { + final Header reasonHeader = response.getFirstHeader("reason"); + if (reasonHeader == null) return new AssertionError("missing 'reason' response header"); + final String sqlState = response.getFirstHeader("sqlstate") != null ? response.getFirstHeader("sqlstate").getValue() : null; + final int vendorCode = response.getFirstHeader("vendorcode") != null ? Integer.parseInt(response.getFirstHeader("vendorcode").getValue()) : 0; + return new SQLException(reasonHeader.getValue(), sqlState, vendorCode); + } - static class PreparedStatementInformation { - ConnectionInformation connectionInformation; - String sql; - Map parameterValues = new HashMap(); + @SneakyThrows + public ResultSet cacheResultSet(ResultSet resultSet) { + CachedRowSet cachedRowSet = RowSetProvider.newFactory().createCachedRowSet(); + cachedRowSet.populate(resultSet); + return cachedRowSet; + } - public PreparedStatementInformation(ConnectionInformation connectionInformation) { - this.connectionInformation = connectionInformation; + @SneakyThrows + protected Object callUnderlyingMethod(Object underlying, Method method, Object[] args, PreparedStatementInformation preparedStatementInformation) { + Object actualResult = method.invoke(underlying, args); + + if (recorder != null && actualResult instanceof ResultSet) { + ResultSet cachedResultSet = cacheResultSet((ResultSet) actualResult); + recorder.writeOutMapping(preparedStatementInformation, cachedResultSet); + cachedResultSet.beforeFirst(); + return cachedResultSet; } - public ConnectionInformation getConnectionInformation() { - return connectionInformation; - } + return actualResult; - public String getSql() { - return sql; - } + } - public Map getParameterValues() { - return parameterValues; - } - public void setStatementQuery(String sql) { - this.sql = sql; - } + @RequiredArgsConstructor + static class PreparedStatementInformation { + @Getter private final ConnectionInformation connectionInformation; + @Getter @Setter private String sql; + @Getter private final Map parameterValues = new HashMap<>(); public void setParameterValue(int position, Object value) { parameterValues.put(position, value); @@ -319,7 +349,7 @@ public P6MockPreparedStatementInvocationHandler(PreparedStatement underlying, super(underlying); PreparedStatementInformation preparedStatementInformation = new PreparedStatementInformation(connectionInformation); - preparedStatementInformation.setStatementQuery(query); + preparedStatementInformation.setSql(query); Delegate executeDelegate = createPreparedStatementExecuteDelegate(preparedStatementInformation); Delegate setParameterValueDelegate = new P6MockPreparedStatementSetParameterValueDelegate(preparedStatementInformation); @@ -366,7 +396,7 @@ protected P6MockPreparedStatementInvocationHandler createPreparedStatementInvoca * @return array with corresponding number of updated rows for each batch */ private static int[] parseBatchUpdateRowsAffected(String responseContent) { - return Stream.of(responseContent.split(",")).mapToInt(s -> Integer.parseInt(s)).toArray(); + return Stream.of(responseContent.split(",")).mapToInt(Integer::parseInt).toArray(); } } diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java index ba2ff0f1..208067b9 100644 --- a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java +++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java @@ -9,6 +9,7 @@ import com.mockrunner.base.NestedApplicationException; import com.mockrunner.mock.jdbc.MockResultSet; +import lombok.Setter; import org.jdom.Document; import org.jdom.Element; import org.jdom.Namespace; @@ -20,6 +21,9 @@ */ public class MockResultSetHelper { + @Setter + private XmlTypeRegistry xmlTypeRegistry = new XmlTypeRegistry(); + /** * Parse a MockResultSet from the provided Sybase-style formatted XML Document. * See Sybase - Using FOR XML RAW documentation and @@ -37,8 +41,8 @@ public class MockResultSetHelper { * * } */ - @SuppressWarnings({"rawtypes", "unchecked"}) - public static MockResultSet parseResultSetFromSybaseXmlString(String id, String xml) { + @SuppressWarnings({"unchecked"}) + public MockResultSet parseResultSetFromSybaseXmlString(String id, String xml) { MockResultSet resultSet = new MockResultSet(id); SAXBuilder builder = new SAXBuilder(); Document doc; @@ -64,10 +68,10 @@ public static MockResultSet parseResultSetFromSybaseXmlString(String id, String return resultSet; } - private static class DatabaseRow { + protected class DatabaseRow { final List colNames; - final Map namedValues; - final String[] positionalValues; + final Map namedValues; + final Object[] positionalValues; int colCount; @@ -75,12 +79,12 @@ public DatabaseRow(List colNames) { this.colNames = colNames; this.colCount = 0; this.namedValues = new HashMap<>(); - this.positionalValues = new String[this.colNames.size()]; + this.positionalValues = new Object[this.colNames.size()]; } public void add(Element col) { String name = getElementName(col); - String val = getNilableElementText(col); + Object val = getNilableElementText(col); if (colNames.contains(name)) { namedValues.put(name, val); } else { @@ -93,7 +97,7 @@ public List toRowValues() { List vals = new ArrayList<>(this.colNames.size()); for(int i=0;i toRowValues() { } } - private static String getNilableElementText(Element col) { + protected Object getNilableElementText(Element col) { String val = col.getText(); if ("true".equalsIgnoreCase(col.getAttributeValue("nil", nsXsi))) { - val = null; + return null; + } + else { + String type = col.getAttributeValue("type", nsXsi); + if (type != null) { + return xmlTypeRegistry.parseValue(type, val); + } } return val; } - private static List parseColumnNames(MockResultSet resultSet, Element root) { + protected List parseColumnNames(MockResultSet resultSet, Element root) { // determine columns Element colsHeaderRow = root.getChild("cols"); List colNames = new ArrayList<>(); @@ -134,7 +144,7 @@ private static List parseColumnNames(MockResultSet resultSet, Element ro return Collections.unmodifiableList(colNames); } - private static String getElementName(Element col) { + protected String getElementName(Element col) { String name = col.getAttributeValue("name"); if (name == null) { name = col.getName(); @@ -142,11 +152,6 @@ private static String getElementName(Element col) { return name; } - @SuppressWarnings("unchecked") - private static List cast(List list) { - return (List) list; - } - @SuppressWarnings("unchecked") private static List cast(List list, Class elementType) { return (List) list; diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorder.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorder.java new file mode 100644 index 00000000..47e23714 --- /dev/null +++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorder.java @@ -0,0 +1,132 @@ +package org.eeichinger.servicevirtualisation.jdbc; + +import lombok.Setter; +import lombok.SneakyThrows; + +import java.io.FileWriter; +import java.io.PrintWriter; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Records called JDBC calls in the mapping json format that WireMock uses (e.g. expectedRecording.json) + * These are saved sequentially in /test/resources/newMappings/, e.g. 1.json, 2.json. This directory should exist. + * Workflow: these files can be renamed and move to the directory that WireMock is expecting (e.g. /test/resources/mappings) + */ +public class WireMockMappingJsonRecorder { + + @Setter + private XmlTypeRegistry xmlTypeRegistry; + + public WireMockMappingJsonRecorder() { + this(false); + } + + public WireMockMappingJsonRecorder(boolean useExplicitTypes) { + if (useExplicitTypes) { + xmlTypeRegistry = new XmlTypeRegistry(); + } + } + + private int statementInTest = 0; + + @SneakyThrows + protected String getSybaseXMLFromDBResult(ResultSet resultSet) { + StringBuilder parameterXml = new StringBuilder(); + parameterXml.append(""); + ResultSetMetaData md = resultSet.getMetaData(); + int numCols = md.getColumnCount(); + List colNames = IntStream.range(0, numCols) + .mapToObj(i -> { + try { + return md.getColumnName(i + 1); + } catch (SQLException e) { + e.printStackTrace(); + return "?"; + } + }).collect(Collectors.toList()); + + colNames.forEach(s -> parameterXml + .append("") + .append(s) + .append("")); + + parameterXml.append(""); + + while (resultSet.next()) { + parameterXml.append(""); + for (String cn : colNames) { + Object rowValue = resultSet.getObject(cn); + if (rowValue == null) { + parameterXml.append(""); + } else { + if (xmlTypeRegistry != null) { + String mappedType = xmlTypeRegistry.getXmlType(rowValue.getClass()); + if (mappedType != null) { + parameterXml.append("").append(rowValue).append(""); + } else if (rowValue instanceof String) { + parameterXml.append("").append(rowValue).append(""); + } else { + throw new RuntimeException("Didn't know how to write out " + rowValue + " of class " + rowValue.getClass()); + } + } else { //always coerce to string + parameterXml.append("").append(rowValue).append(""); + } + } + } + parameterXml.append(""); + } + + parameterXml.append(""); + + return parameterXml.toString(); + } + + protected String getRecordFilePath(int statementInTest) { + return "src/test/resources/newMappings/" + statementInTest + ".json"; + } + + @SneakyThrows + public void writeOutMapping(JdbcServiceVirtualizationFactory.PreparedStatementInformation preparedStatementInformation, ResultSet resultSet) { + PrintWriter printWriter = new PrintWriter(new FileWriter(getRecordFilePath(++statementInTest))); + printWriter.print("{\n" + + " \"request\": {\n" + + " \"method\": \"POST\",\n" + + " \"url\": \"/sqlstub\",\n" + + " \"bodyPatterns\":\n" + + " [\n" + + " {\n" + + " \"equalTo\": \""); + printWriter.print(preparedStatementInformation.getSql()); + printWriter.print("\"\n" + + " }\n" + + " ],\n" + + " \"headers\": {\n" + + " "); + + String paramHeaders = preparedStatementInformation.getParameterValues().entrySet().stream().map(entry -> String.format(" \"%d\": {\n" + + " \"equalTo\": \"%s\"\n" + + " }", entry.getKey(), entry.getValue())).collect(Collectors.joining(",\n" + + " ")); + printWriter.print(paramHeaders); + + printWriter.print("}\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200,\n" + + " \"body\": \""); + + printWriter.print(getSybaseXMLFromDBResult(resultSet)); + + printWriter.print("\"\n" + + " }\n" + + "}"); + + printWriter.close(); + + } +} diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/XmlTypeRegistry.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/XmlTypeRegistry.java new file mode 100644 index 00000000..fc661a13 --- /dev/null +++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/XmlTypeRegistry.java @@ -0,0 +1,49 @@ +package org.eeichinger.servicevirtualisation.jdbc; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class XmlTypeRegistry { + + private static final List> DEFAULT_TYPES = new ArrayList<>(); + static { + DEFAULT_TYPES.add(new XmlTypeInfo<>(BigDecimal.class, "xs:decimal", BigDecimal::new)); + DEFAULT_TYPES.add(new XmlTypeInfo<>(Integer.class, "xs:integer", Integer::new)); + DEFAULT_TYPES.add(new XmlTypeInfo<>(Long.class, "xs:long", Long::new)); + } + + @Getter + @Setter + private List> xmlTypeInfoList = DEFAULT_TYPES; + + public String getXmlType(Class clazz) { + return DEFAULT_TYPES.stream() + .filter(xmlTypeInfo -> xmlTypeInfo.clazz.equals(clazz)) + .findAny() + .map(xmlTypeInfo -> xmlTypeInfo.xmlType) + .orElse(null); + } + + public Object parseValue(String xmlType, String val) { + return DEFAULT_TYPES.stream() + .filter(xmlTypeInfo -> xmlTypeInfo.xmlType.equals(xmlType)) + .findAny() + .map(xmlTypeInfo -> xmlTypeInfo.mapper.apply(val)) + .orElse(val); + } + + @Data + @AllArgsConstructor + public static class XmlTypeInfo { + Class clazz; + String xmlType; + Function mapper; + } +} diff --git a/src/test/java/example/UseWireMockToMockJdbcResultSetsTest.java b/src/test/java/example/UseWireMockToMockJdbcResultSetsTest.java index 29be916d..2ab0aff2 100644 --- a/src/test/java/example/UseWireMockToMockJdbcResultSetsTest.java +++ b/src/test/java/example/UseWireMockToMockJdbcResultSetsTest.java @@ -1,11 +1,5 @@ package example; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.List; - -import javax.sql.DataSource; - import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockRule; import lombok.Value; @@ -18,8 +12,14 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; /** * Demonstrates you to use the technique to just spy on a real database and intercept/mock only selected jdbc queries @@ -121,6 +121,47 @@ public void can_mock_nullvalues() { assertThat(matthias.getPlaceOfBirth(), nullValue()); } + @Test + public void can_mock_types() { + // setup mock resultsets + WireMock.stubFor(WireMock + .post(WireMock.urlPathEqualTo("/sqlstub")) + // SQL Statement is posted in the body, use any available matchers to match + .withRequestBody(WireMock.equalTo("SELECT * FROM PEOPLE WHERE name=?")) + // return a recordset + .willReturn(WireMock + .aResponse() + .withHeader("content-type", "application/xml; charset=utf-8") + .withBody("" + + "" + + "nameage" + // xsi:type can be set on the column + + " Erich Eichinger10" + + " Matthias Bernlöhr20" + + "" + ) + ) + ); + + RowMapper rowMapper = (rs, rowNum) -> + new NameAge(rs.getString("name"), + (int) rs.getObject("age")); //NB getObject can be used since the type is known to the ResultSet + + List result = jdbcTemplate + .query( + "SELECT * FROM PEOPLE WHERE name=?" + , rowMapper + , args("Erich Eichinger") + ); + + NameAge erich = result.get(0); + assertThat(erich.getName(), equalTo("Erich Eichinger")); + assertThat(erich.getAge(), equalTo(10)); + NameAge matthias = result.get(1); + assertThat(matthias.getName(), equalTo("Matthias Bernlöhr")); + assertThat(matthias.getAge(), equalTo(20)); + } + @Test public void intercepts_matching_query_and_responds_with_mockresultset() { final String NAME_ERICH_EICHINGER = "Erich Eichinger"; @@ -372,6 +413,12 @@ private static class Person { String placeOfBirth; } + @Value + private static class NameAge { + String name; + int age; + } + private static Object[] args(Object... args) { return args; } diff --git a/src/test/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelperTest.java b/src/test/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelperTest.java index d8456b55..41ed1be5 100644 --- a/src/test/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelperTest.java +++ b/src/test/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelperTest.java @@ -32,7 +32,7 @@ public void can_parse_sybase_element_dialect() throws Exception { System.out.println(xml); - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(4)); @@ -63,7 +63,7 @@ public void determine_column_names_from_cols_row() throws Exception { + " James Bond1900-01-04Philadelphia\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); @@ -81,7 +81,7 @@ public void determine_column_names_from_first_row_element_names_or_attribute() t + " James BondPhiladelphia\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); @@ -100,7 +100,7 @@ public void parse_nil_attribute_as_null() throws Exception { + " James BondPhiladelphia\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); @@ -119,7 +119,7 @@ public void parse_empty_element_as_empty_string() throws Exception { + " James BondPhiladelphia\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); @@ -138,7 +138,7 @@ public void parse_missing_named_element_as_null() throws Exception { + " James BondPhiladelphia\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); @@ -157,7 +157,7 @@ public void parse_columns_in_order_of_column_names() throws Exception { + " PhiladelphiaJames Bond\n" + "\n"; - final MockResultSet resultSet = MockResultSetHelper.parseResultSetFromSybaseXmlString("x", xml); + final MockResultSet resultSet = new MockResultSetHelper().parseResultSetFromSybaseXmlString("x", xml); assertThat(resultSet.getColumnCount(), equalTo(3)); assertThat(resultSet.getRowCount(), equalTo(1)); diff --git a/src/test/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorderTest.java b/src/test/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorderTest.java new file mode 100644 index 00000000..5497efb4 --- /dev/null +++ b/src/test/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorderTest.java @@ -0,0 +1,90 @@ +package org.eeichinger.servicevirtualisation.jdbc; + +import com.mockrunner.mock.jdbc.MockResultSet; +import com.p6spy.engine.common.ConnectionInformation; +import lombok.SneakyThrows; +import org.junit.Test; + +import java.io.File; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + + +@SuppressWarnings("ResultOfMethodCallIgnored") +public class WireMockMappingJsonRecorderTest { + @Test + @SneakyThrows + public void testWireMockRecording() { + WireMockMappingJsonRecorder recorder = new WireMockMappingJsonRecorder(); + JdbcServiceVirtualizationFactory.PreparedStatementInformation preparedStatementInformation = new JdbcServiceVirtualizationFactory.PreparedStatementInformation(new ConnectionInformation()); + preparedStatementInformation.setSql("SELECT birthday, placeofbirth FROM PEOPLE WHERE name = ?"); + preparedStatementInformation.setParameterValue(0, "Erich Eichinger"); + + MockResultSet resultSet = new MockResultSet("x"); + resultSet.addColumn("birthday"); + resultSet.addColumn("placeofbirth"); + resultSet.addRow(Arrays.asList("1980-01-01", "Vienna")); + + new File("src/test/resources/newMappings/").mkdirs(); + recorder.writeOutMapping(preparedStatementInformation, resultSet); + + List expectedLines = Files.readAllLines(Paths.get("src/test/resources/expectedRecording.json")); + List actualLines = Files.readAllLines(Paths.get("src/test/resources/newMappings/1.json")); + + assertThat(actualLines, equalTo(expectedLines)); + } + + @Test + public void testJsonsAreAddedSequentially() { + new File("src/test/resources/newMappings/").mkdirs(); + for (File file : Objects.requireNonNull(new File("src/test/resources/newMappings/").listFiles())) { + file.delete(); + } + + assertThat(new File("src/test/resources/newMappings/1.json").exists(), equalTo(false)); + assertThat(new File("src/test/resources/newMappings/2.json").exists(), equalTo(false)); + + WireMockMappingJsonRecorder recorder = new WireMockMappingJsonRecorder(); + JdbcServiceVirtualizationFactory.PreparedStatementInformation preparedStatementInformation = new JdbcServiceVirtualizationFactory.PreparedStatementInformation(new ConnectionInformation()); + MockResultSet resultSet = new MockResultSet("x"); + + recorder.writeOutMapping(preparedStatementInformation, resultSet); + assertThat(new File("src/test/resources/newMappings/1.json").exists(), equalTo(true)); + assertThat(new File("src/test/resources/newMappings/2.json").exists(), equalTo(false)); + + recorder.writeOutMapping(preparedStatementInformation, resultSet); + assertThat(new File("src/test/resources/newMappings/1.json").exists(), equalTo(true)); + assertThat(new File("src/test/resources/newMappings/2.json").exists(), equalTo(true)); + + } + + @Test + @SneakyThrows + public void testTypeMappings() { + WireMockMappingJsonRecorder recorder = new WireMockMappingJsonRecorder(true); + JdbcServiceVirtualizationFactory.PreparedStatementInformation preparedStatementInformation = new JdbcServiceVirtualizationFactory.PreparedStatementInformation(new ConnectionInformation()); + preparedStatementInformation.setSql("SELECT *"); + + MockResultSet resultSet = new MockResultSet("x"); + resultSet.addColumn("decimal"); + resultSet.addColumn("int"); + resultSet.addColumn("long"); + resultSet.addColumn("string"); + resultSet.addRow(Arrays.asList(new BigDecimal("14.33"), 2, 3L, "hello")); + + new File("src/test/resources/newMappings/").mkdirs(); + recorder.writeOutMapping(preparedStatementInformation, resultSet); + + List expectedLines = Files.readAllLines(Paths.get("src/test/resources/expectedRecordingWithTypes.json")); + List actualLines = Files.readAllLines(Paths.get("src/test/resources/newMappings/1.json")); + + assertThat(actualLines, equalTo(expectedLines)); + } +} diff --git a/src/test/resources/expectedRecording.json b/src/test/resources/expectedRecording.json new file mode 100644 index 00000000..b1ded01a --- /dev/null +++ b/src/test/resources/expectedRecording.json @@ -0,0 +1,20 @@ +{ + "request": { + "method": "POST", + "url": "/sqlstub", + "bodyPatterns": + [ + { + "equalTo": "SELECT birthday, placeofbirth FROM PEOPLE WHERE name = ?" + } + ], + "headers": { + "0": { + "equalTo": "Erich Eichinger" + }} + }, + "response": { + "status": 200, + "body": "birthdayplaceofbirth1980-01-01Vienna" + } +} diff --git a/src/test/resources/expectedRecordingWithTypes.json b/src/test/resources/expectedRecordingWithTypes.json new file mode 100644 index 00000000..44e67380 --- /dev/null +++ b/src/test/resources/expectedRecordingWithTypes.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "url": "/sqlstub", + "bodyPatterns": + [ + { + "equalTo": "SELECT *" + } + ], + "headers": { + } + }, + "response": { + "status": 200, + "body": "decimalintlongstring14.3323hello" + } +}