kevin cline
kevin cline

Reputation: 2736

How to workaround unwanted TestNG assertEquals overloads

I am having a problem where some class X extends extends java.util.AbstractMap, and also overloads equals(Object). Calling org.testng.Assert.assertEquals(X a, X b) resolves to assertEquals(Map<?,?>, Map<?,?>). Instead of calling the 'equals' method, the map entries are compared. This results in assertEquals(a, b) passing even though a.equals(b) is false.

This code demonstrates the problem:

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import java.util.AbstractMap;
import java.util.Collections;
import java.util.Set;

import org.testng.annotations.Test;

public class AssertTest {
    static class X extends AbstractMap<String, Object> {
        private int i;

        public X(int i) {
        this.i = i;
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
        return Collections.EMPTY_SET;
        }

        @Override
        public boolean equals(Object o) {
        return o instanceof X && i == ((X)o).i;
        }

        @Override
        public int hashCode() {
        return i;
        }
    }

    @Test
    public void test() {
        X one = new X(1);
        X two = new X(2);
        assertEquals(one, two); // passes, should fail IMO
        assertTrue(one.equals(two)); // correctly fails
    }
}

What is the best way to work around this problem? One possibility is to remember to not use assertEquals to verify that instances of X match, but that is extremely error-prone.

Another possibility is to simply make a local copy of testng and rename the overloads. That creates an ongoing maintenance problem.

The only other idea I have is to create a project-specific Assert class that delegates to testng.Assert but renames the problematic overloads to 'assertCollectionEquals', 'assertMapEquals', etc.

Upvotes: 1

Views: 3304

Answers (1)

assylias
assylias

Reputation: 328608

org.testng.Assert.assertEquals(X a, X b) calls the assertEquals(Map, Map) method which iterates over the entry set and checks that all entries are equals, bypassing the Map#equals method, as you have noticed.

A simple cast would avoid calling that method and would use the X#equals method instead:

assertEquals((Object) one, (Object) two);

You could also declare your variables as objects to achieve the same result:

Object one = new X(1);
Object two = new X(2);
assertEquals(one, two);

That does not realy solve your issue in the sense that it still is error prone.

To avoid the occasional error, there are a few workarounds I can think of (getting tired here so some might make little sense), using the fact that assertEquals without cast calls x.entrySet():

  • write some code that parses all your test files for X class and make sure that there is a cast in place (not straightforward) or that no X is declared on the left of an =
  • use a mocking framework and mock X in a @BeforeGroups method (if you use groups) and put each test that relies on that idiom in the same group, so that they will use a mocked X where entrySet fails your tests (and the other methods work as expected), or at least logs/prints some warning. If you also need to use entrySet in the same method that won't work
  • use a mocking framework to mock TestNG#assertEquals(Map, Map) to get the desired behaviour
  • in all the test classes that need that idiom, create an ad hoc assertEquals(Map, Map) method. If you have a static import of all assertEquals method you will have to reimplement the other signatures too :-(
  • have all those test classes inherit a BaseXTestClass where you implement an assertEquals(Map, Map) method - same caveat

Upvotes: 3

Related Questions