Repository: assertj/assertj (Java)

Issue:
hasMethods does not work on default methods
#### Summary

IMO `hasMethod` should really follow the same contract as `Class.getMethod()`, but it doesn't when it comes to default methods inherited from an interface (that are not overridden with an implementation in the class being asserted).

This is because `org.assertj.core.internal.Classes.getAllMethods(Class<?>)` only looks at parent classes and not any implemented interfaces.

#### Example

```java
class DefaultHasMethodTest {

  interface HasDefault {
    default void method() {}
  }

  static class Impl implements HasDefault {
  }

  @Test
  void testHasMethod() throws Exception {
    assertThat(Impl.class).hasMethods("method");
  }
}
```


This might be related to #880.


Hints:
Thanks for reporting this, I agree this should be taken into account.
Currently, `hasMethods` doesn't follow `Class::getMethods` not only because of ignoring the default methods but also because it looks into non-public methods declared by `actual` or any of its superclasses.

On the other side, `hasDeclaredMethods` looks into the methods of `actual` with any visibility but excluding the inherited ones, in line with `Class::getDeclaredMethods`.

I am wondering if `hasMethods` should only gain interface lookup but still searching for methods of any visibility, or if it should focus only on public methods and be fully aligned with `Class::getMethods`. The latter would be a breaking change but in sync with the JDK behavior.
I think I would go with adding default methods to `hasMethods` and keep looking for methods of any visibility.

We have `hasPublicMethods` that only look at `Class::getMethods`, there is a discrepancy between AssertJ and the JDK here but I feel it is not that bad as `hasPublicMethods` is more obvious than `Class::getMethods` at indicating that it only looks at public methods.


I missed `hasPublicMethods`, then yes, I'd add default method lookup to `hasMethods`. I have a fix almost ready so I'll grab this issue.

Patch:
diff --git a/src/main/java/org/assertj/core/internal/Classes.java b/src/main/java/org/assertj/core/internal/Classes.java
--- a/src/main/java/org/assertj/core/internal/Classes.java
+++ b/src/main/java/org/assertj/core/internal/Classes.java
@@ -431,7 +431,7 @@ private static Set<String> fieldsToName(Set<Field> fields) {
    * @param actual the "actual" {@code Class}.
    * @param methods the methods who must be present in the class.
    * @throws AssertionError if {@code actual} is {@code null}.
-   * @throws AssertionError if the actual {@code Class} doesn't contains all of the methods.
+   * @throws AssertionError if the actual {@code Class} doesn't contains all the methods.
    */
   public void assertHasMethods(AssertionInfo info, Class<?> actual, String... methods) {
     assertNotNull(info, actual);
@@ -445,7 +445,7 @@ public void assertHasMethods(AssertionInfo info, Class<?> actual, String... meth
    * @param actual the "actual" {@code Class}.
    * @param methods the methods who must be declared in the class.
    * @throws AssertionError if {@code actual} is {@code null}.
-   * @throws AssertionError if the actual {@code Class} doesn't contains all of the methods.
+   * @throws AssertionError if the actual {@code Class} doesn't contains all the methods.
    */
   public void assertHasDeclaredMethods(AssertionInfo info, Class<?> actual, String... methods) {
     assertNotNull(info, actual);
@@ -476,7 +476,7 @@ private void doAssertHasMethods(AssertionInfo info, Class<?> actual, Set<Method>
    * @param actual the "actual" {@code Class}.
    * @param methods the public methods who must be present in the class.
    * @throws AssertionError if {@code actual} is {@code null}.
-   * @throws AssertionError if the actual {@code Class} doesn't contains all of the public methods.
+   * @throws AssertionError if the actual {@code Class} doesn't contains all the public methods.
    */
   public void assertHasPublicMethods(AssertionInfo info, Class<?> actual, String... methods) {
     assertNotNull(info, actual);
@@ -555,7 +555,10 @@ private static Method[] getAllMethods(Class<?> actual) {
     if (superclass != null) {
       allMethods.addAll(newLinkedHashSet(getAllMethods(superclass)));
     }
-    return allMethods.toArray(new Method[allMethods.size()]);
+    for (Class<?> anInterface : actual.getInterfaces()) {
+      allMethods.addAll(newLinkedHashSet(getAllMethods(anInterface)));
+    }
+    return allMethods.toArray(new Method[0]);
   }
 
   private static <M extends Member> Set<M> filterSyntheticMembers(M[] members) {


Test patch:
diff --git a/src/test/java/org/assertj/core/internal/classes/Classes_assertHasMethods_Test.java b/src/test/java/org/assertj/core/internal/classes/Classes_assertHasMethods_Test.java
--- a/src/test/java/org/assertj/core/internal/classes/Classes_assertHasMethods_Test.java
+++ b/src/test/java/org/assertj/core/internal/classes/Classes_assertHasMethods_Test.java
@@ -51,17 +51,17 @@ void should_pass_if_actual_has_expected_accessible_public_methods() {
   @Test
   void should_fail_if_no_methods_are_expected_and_methods_are_available() {
     SortedSet<String> expectedMethods = newTreeSet("publicMethod",
-                                                 "protectedMethod",
-                                                 "privateMethod",
-                                                 "finalize",
-                                                 "wait",
-                                                 "equals",
-                                                 "toString",
-                                                 "hashCode",
-                                                 "getClass",
-                                                 "clone",
-                                                 "notify",
-                                                 "notifyAll");
+                                                   "protectedMethod",
+                                                   "privateMethod",
+                                                   "finalize",
+                                                   "wait",
+                                                   "equals",
+                                                   "toString",
+                                                   "hashCode",
+                                                   "getClass",
+                                                   "clone",
+                                                   "notify",
+                                                   "notifyAll");
     if (isJavaVersionBefore14()) {
       expectedMethods.add("registerNatives");
     }
@@ -98,4 +98,39 @@ private static boolean isJavaVersionBefore14() {
     BigDecimal javaVersion = new BigDecimal(System.getProperty("java.specification.version"));
     return javaVersion.compareTo(new BigDecimal("14")) < 0;
   }
+
+  @Test
+  void should_pass_with_direct_default_method() {
+    // GIVEN
+    Class<?> actual = ClassWithDirectDefaultMethod.class;
+    String[] expected = { "method" };
+    // WHEN/THEN
+    classes.assertHasMethods(someInfo(), actual, expected);
+  }
+
+  private static class ClassWithDirectDefaultMethod implements InterfaceWithDefaultMethod {
+  }
+
+  private interface InterfaceWithDefaultMethod {
+
+    @SuppressWarnings("unused")
+    default void method() {}
+
+  }
+
+  @Test
+  void should_pass_with_indirect_default_method() {
+    // GIVEN
+    Class<?> actual = ClassWithIndirectDefaultMethod.class;
+    String[] expected = { "method" };
+    // WHEN/THEN
+    classes.assertHasMethods(someInfo(), actual, expected);
+  }
+
+  private static class ClassWithIndirectDefaultMethod implements InterfaceInheritingDefaultMethod {
+  }
+
+  private interface InterfaceInheritingDefaultMethod extends InterfaceWithDefaultMethod {
+  }
+
 }