Extract generic concept
diff --git a/src/main/java/org/testng/internal/AbstractExpectedExceptionsHolder.java b/src/main/java/org/testng/internal/AbstractExpectedExceptionsHolder.java
new file mode 100644
index 0000000..204d87c
--- /dev/null
+++ b/src/main/java/org/testng/internal/AbstractExpectedExceptionsHolder.java
@@ -0,0 +1,76 @@
+package org.testng.internal;
+
+import org.testng.ITestNGMethod;
+import org.testng.TestException;
+
+import java.util.Arrays;
+
+public abstract class AbstractExpectedExceptionsHolder {
+
+  private final Class<?>[] expectedClasses;
+
+  protected AbstractExpectedExceptionsHolder(Class<?>[] expectedClasses) {
+    this.expectedClasses = expectedClasses;
+  }
+
+  /**
+   * @param ite The exception that was just thrown
+   * @return true if the exception that was just thrown is part of the
+   * expected exceptions
+   */
+  public boolean isExpectedException(Throwable ite) {
+    if (expectedClasses == null) {
+      return false;
+    }
+
+    // TestException is the wrapper exception that TestNG will be throwing when an exception was
+    // expected but not thrown
+    if (ite.getClass() == TestException.class) {
+      return false;
+    }
+
+    Class<?> realExceptionClass= ite.getClass();
+
+    for (Class<?> exception : expectedClasses) {
+      if (exception.isAssignableFrom(realExceptionClass) && isExceptionMatches(ite)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  public TestException wrongException(Throwable ite) {
+    if (isExceptionMatches(ite)) {
+      return new TestException("Expected exception of " +
+                               getExpectedExceptionsPluralize()
+                               + " but got " + ite, ite);
+    } else {
+      return new TestException(getWrongExceptionMessage(ite), ite);
+    }
+  }
+
+  protected abstract String getWrongExceptionMessage(Throwable ite);
+
+  public TestException noException(ITestNGMethod testMethod) {
+    if (expectedClasses == null || expectedClasses.length == 0) {
+      return null;
+    }
+    return new TestException("Method " + testMethod + " should have thrown an exception of "
+                             + getExpectedExceptionsPluralize());
+  }
+
+  private String getExpectedExceptionsPluralize() {
+    StringBuilder sb = new StringBuilder();
+    if (expectedClasses.length > 1) {
+      sb.append("any of types ");
+      sb.append(Arrays.toString(expectedClasses));
+    } else {
+      sb.append("type ");
+      sb.append(expectedClasses[0]);
+    }
+    return sb.toString();
+  }
+
+  protected abstract boolean isExceptionMatches(Throwable ite);
+}
diff --git a/src/main/java/org/testng/internal/ExpectedExceptionsHolder.java b/src/main/java/org/testng/internal/ExpectedExceptionsHolder.java
index 46a3a4b..0894340 100755
--- a/src/main/java/org/testng/internal/ExpectedExceptionsHolder.java
+++ b/src/main/java/org/testng/internal/ExpectedExceptionsHolder.java
@@ -10,48 +10,21 @@
  * A class that contains the expected exceptions and the message regular expression.
  * @author cbeust
  */
-public class ExpectedExceptionsHolder {
-  private final Class<?>[] expectedClasses;
+public class ExpectedExceptionsHolder extends AbstractExpectedExceptionsHolder {
   private final String messageRegExp;
 
   public ExpectedExceptionsHolder(Class<?>[] expectedClasses, String messageRegExp) {
-    this.expectedClasses = expectedClasses;
+    super(expectedClasses);
     this.messageRegExp = messageRegExp;
   }
 
   /**
-   * @param ite The exception that was just thrown
-   * @return true if the exception that was just thrown is part of the
-   * expected exceptions
-   */
-  public boolean isExpectedException(Throwable ite) {
-    if (expectedClasses == null) {
-      return false;
-    }
-
-    // TestException is the wrapper exception that TestNG will be throwing when an exception was
-    // expected but not thrown
-    if (ite.getClass() == TestException.class) {
-      return false;
-    }
-
-    Class<?> realExceptionClass= ite.getClass();
-
-    for (Class<?> exception : expectedClasses) {
-      if (exception.isAssignableFrom(realExceptionClass) && messageRegExpMatches(ite)) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  /**
    *   message / regEx  .*      other
    *   null             true    false
    *   non-null         true    match
    */
-  private boolean messageRegExpMatches(Throwable ite) {
+  @Override
+  protected boolean isExceptionMatches(Throwable ite) {
     if (".*".equals(messageRegExp)) {
       return true;
     } else {
@@ -60,35 +33,9 @@
     }
   }
 
-  public TestException wrongException(Throwable ite) {
-    if (messageRegExpMatches(ite)) {
-      return new TestException("Expected exception of " +
-                               getExpectedExceptionsPluralize()
-                               + " but got " + ite, ite);
-    } else {
-      return new TestException("The exception was thrown with the wrong message:" +
-                               " expected \"" + messageRegExp + "\"" +
-                               " but got \"" + ite.getMessage() + "\"", ite);
-    }
-  }
-
-  public TestException noException(ITestNGMethod testMethod) {
-    if (expectedClasses == null || expectedClasses.length == 0) {
-      return null;
-    }
-    return new TestException("Method " + testMethod + " should have thrown an exception of "
-                             + getExpectedExceptionsPluralize());
-  }
-
-  private String getExpectedExceptionsPluralize() {
-    StringBuilder sb = new StringBuilder();
-    if (expectedClasses.length > 1) {
-      sb.append("any of types ");
-      sb.append(Arrays.toString(expectedClasses));
-    } else {
-      sb.append("type ");
-      sb.append(expectedClasses[0]);
-    }
-    return sb.toString();
+  protected String getWrongExceptionMessage(Throwable ite) {
+    return "The exception was thrown with the wrong message:" +
+           " expected \"" + messageRegExp + "\"" +
+           " but got \"" + ite.getMessage() + "\"";
   }
 }