diff --git a/libraries/bot-dialogs/pom.xml b/libraries/bot-dialogs/pom.xml
index c51f1509b..6e12adc70 100644
--- a/libraries/bot-dialogs/pom.xml
+++ b/libraries/bot-dialogs/pom.xml
@@ -158,4 +158,15 @@
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ -Dfile.encoding=UTF-8
+
+
+
+
diff --git a/libraries/bot-dialogs/src/test/java/com/microsoft/recognizers/text/tests/AbstractTest.java b/libraries/bot-dialogs/src/test/java/com/microsoft/recognizers/text/tests/AbstractTest.java
new file mode 100644
index 000000000..dcde7040a
--- /dev/null
+++ b/libraries/bot-dialogs/src/test/java/com/microsoft/recognizers/text/tests/AbstractTest.java
@@ -0,0 +1,325 @@
+package com.microsoft.recognizers.text.tests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.recognizers.text.*;
+import com.microsoft.recognizers.text.datetime.parsers.DateTimeParseResult;
+import com.microsoft.recognizers.text.tests.helpers.DateTimeParseResultMixIn;
+import com.microsoft.recognizers.text.tests.helpers.ExtendedModelResultMixIn;
+import com.microsoft.recognizers.text.tests.helpers.ExtractResultMixIn;
+import com.microsoft.recognizers.text.tests.helpers.ModelResultMixIn;
+import org.apache.commons.io.FileUtils;
+import org.javatuples.Pair;
+import org.junit.*;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@RunWith(Parameterized.class)
+public abstract class AbstractTest {
+
+ private static final String SpecsPath = "Specs/..";
+
+ private static final List SupportedCultures = Arrays.asList("English", "Spanish", "Portuguese", "French", "German", "Chinese");
+
+ // FEFF - UTF-8 byte order mark (EF BB BF) as Unicode char representation.
+ private static final String UTF8_BOM = "\uFEFF";
+
+ protected final TestCase currentCase;
+
+ public AbstractTest(TestCase currentCase) {
+ this.currentCase = currentCase;
+ }
+
+ private static Map testCounter;
+ private static Map passCounter;
+ private static Map failCounter;
+ private static Map skipCounter;
+
+ @BeforeClass
+ public static void before() {
+ testCounter = new LinkedHashMap<>();
+ passCounter = new LinkedHashMap<>();
+ failCounter = new LinkedHashMap<>();
+ skipCounter = new LinkedHashMap<>();
+ }
+
+ @AfterClass
+ public static void after() {
+
+ Map counter = new LinkedHashMap<>();
+
+ for (Map.Entry entry : testCounter.entrySet()) {
+ int skipped = skipCounter.getOrDefault(entry.getKey(), 0);
+ if (entry.getValue() > skipped) {
+ counter.put(entry.getKey(), String.format("%7d", entry.getValue()));
+ }
+ }
+
+ for (Map.Entry entry : counter.entrySet()) {
+ Integer passValue = passCounter.getOrDefault(entry.getKey(), 0);
+ Integer failValue = failCounter.getOrDefault(entry.getKey(), 0);
+ Integer skipValue = skipCounter.getOrDefault(entry.getKey(), 0);
+ counter.put(entry.getKey(), String.format("|%s |%7d |%7d |%7d ", entry.getValue(), passValue, skipValue, failValue));
+ }
+
+ print(counter);
+ }
+
+ private static void print(Map map) {
+ System.out.println("| TOTAL | Passed | Skipped | Failed || Key");
+ for (Map.Entry entry : map.entrySet()) {
+ System.out.println(entry.getValue() + "|| " + entry.getKey());
+ }
+ }
+
+ private void count(TestCase testCase) {
+ String key = testCase.recognizerName + "-" + testCase.language + "-" + testCase.modelName;
+ Integer current = testCounter.getOrDefault(key, 0);
+ testCounter.put(key, current + 1);
+ }
+
+ private void countPass(TestCase testCase) {
+ String key = testCase.recognizerName + "-" + testCase.language + "-" + testCase.modelName;
+ Integer current = passCounter.getOrDefault(key, 0);
+ passCounter.put(key, current + 1);
+ }
+
+ private void countSkip(TestCase testCase) {
+ String key = testCase.recognizerName + "-" + testCase.language + "-" + testCase.modelName;
+ Integer current = skipCounter.getOrDefault(key, 0);
+ skipCounter.put(key, current + 1);
+ }
+
+ private void countFail(TestCase testCase) {
+ String key = testCase.recognizerName + "-" + testCase.language + "-" + testCase.modelName;
+ Integer current = failCounter.getOrDefault(key, 0);
+ failCounter.put(key, current + 1);
+ }
+
+ @Test
+ public void test() {
+
+ count(currentCase);
+
+ if (!isJavaSupported(this.currentCase.notSupported)) {
+ countSkip(currentCase);
+ throw new AssumptionViolatedException("Test case wih input '" + this.currentCase.input + "' not supported.");
+ }
+
+ if (this.currentCase.debug) {
+ // Add breakpoint here to stop on those TestCases marked with "Debug": true
+ System.out.println("Debug Break!");
+ }
+
+ try {
+ recognizeAndAssert(currentCase);
+ countPass(this.currentCase);
+ } catch (AssumptionViolatedException ex) {
+ countSkip(currentCase);
+ throw ex;
+ } catch (Throwable err) {
+ countFail(currentCase);
+ throw err;
+ }
+ }
+
+ // TODO Override in specific models
+ protected abstract List recognize(TestCase currentCase);
+
+ protected void recognizeAndAssert(TestCase currentCase) {
+ List results = recognize(currentCase);
+ assertResults(currentCase, results);
+ }
+
+ public static void assertResults(TestCase currentCase, List results) {
+ assertResultsWithKeys(currentCase, results, Collections.emptyList());
+ }
+
+ public static void assertResultsWithKeys(TestCase currentCase, List results, List testResolutionKeys) {
+
+ List expectedResults = readExpectedResults(ModelResult.class, currentCase.results);
+ Assert.assertEquals(getMessage(currentCase, "\"Result Count\""), expectedResults.size(), results.size());
+
+ IntStream.range(0, expectedResults.size())
+ .mapToObj(i -> Pair.with(expectedResults.get(i), results.get(i)))
+ .forEach(t -> {
+ ModelResult expected = t.getValue0();
+ ModelResult actual = t.getValue1();
+
+ Assert.assertEquals(getMessage(currentCase, "typeName"), expected.typeName, actual.typeName);
+ Assert.assertEquals(getMessage(currentCase, "text"), expected.text, actual.text);
+
+ if (expected.resolution.containsKey(ResolutionKey.Value)) {
+ Assert.assertEquals(getMessage(currentCase, "resolution.value"),
+ expected.resolution.get(ResolutionKey.Value), actual.resolution.get(ResolutionKey.Value));
+ }
+
+ for (String key : testResolutionKeys) {
+ Assert.assertEquals(getMessage(currentCase, key), expected.resolution.get(key), actual.resolution.get(key));
+ }
+ });
+ }
+
+ public static Collection enumerateTestCases(String recognizerType, String modelName) {
+
+ String recognizerTypePath = String.format(File.separator + recognizerType + File.separator);
+
+ // Deserializer
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+
+ // Map json to TestCases
+ return FileUtils.listFiles(new File(SpecsPath), new String[]{"json"}, true)
+ .stream().filter(f -> f.getPath().contains(recognizerTypePath))
+ .map(f -> parseSpecFile(f, mapper))
+ .flatMap(ts -> Arrays.stream(ts))
+ // Ignore tests with NotSupportedByDesign = Java
+ .filter(ts -> isJavaSupported(ts.notSupportedByDesign))
+ // Filter supported languages only
+ .filter(ts -> SupportedCultures.contains(ts.language))
+ .filter(ts -> ts.modelName.contains(modelName))
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ public static TestCase[] parseSpecFile(File f, ObjectMapper mapper) {
+
+ List paths = Arrays.asList(f.toPath().toString().split(Pattern.quote(File.separator)));
+ List testInfo = paths.subList(paths.size() - 3, paths.size());
+
+ try {
+
+ // Workaround to consume a possible UTF-8 BOM byte
+ // https://stackoverflow.com/questions/4897876/reading-utf-8-bom-marker
+ String contents = new String(Files.readAllBytes(f.toPath()));
+ String json = StringUtf8Bom(contents);
+
+ TestCase[] tests = mapper.readValue(json, TestCase[].class);
+ Arrays.stream(tests).forEach(t -> {
+ t.recognizerName = testInfo.get(0);
+ t.language = testInfo.get(1);
+ t.modelName = testInfo.get(2).split(Pattern.quote("."))[0];
+ });
+
+ return tests;
+
+ } catch (IOException ex) {
+
+ System.out.println("Error reading Spec file: " + f.toString() + " | " + ex.getMessage());
+
+ // @TODO: This should cause a test run failure.
+ return new TestCase[0];
+ }
+ }
+
+ public static T parseExtractResult(Class extractorResultClass, Object result) {
+ // Deserializer
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+ mapper.addMixIn(ExtractResult.class, ExtractResultMixIn.class);
+
+ try {
+ String json = mapper.writeValueAsString(result);
+ return mapper.readValue(json, extractorResultClass);
+
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ return null;
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static T parseDateTimeParseResult(Class dateTimeParseResultClass, Object result) {
+ // Deserializer
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+ mapper.addMixIn(DateTimeParseResult.class, DateTimeParseResultMixIn.class);
+
+ try {
+ String json = mapper.writeValueAsString(result);
+ return mapper.readValue(json, dateTimeParseResultClass);
+
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ return null;
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static T parseResult(Class modelResultClass, Object result) {
+ // Deserializer
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+ mapper.addMixIn(ModelResult.class, ModelResultMixIn.class);
+ mapper.addMixIn(ExtendedModelResult.class, ExtendedModelResultMixIn.class);
+
+ try {
+ String json = mapper.writeValueAsString(result);
+ return mapper.readValue(json, modelResultClass);
+
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ return null;
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static List readExpectedResults(Class modelResultClass, List