Como afirmo a igualdade em duas classes sem um método igual?

111

Digamos que eu tenha uma classe sem o método equals (), para a qual não tenho a fonte. Eu quero afirmar a igualdade em duas instâncias dessa classe.

Posso fazer várias afirmações:

assertEquals(obj1.getFieldA(), obj2.getFieldA());
assertEquals(obj1.getFieldB(), obj2.getFieldB());
assertEquals(obj1.getFieldC(), obj2.getFieldC());
...

Não gosto dessa solução porque não consigo entender o quadro completo da igualdade se uma declaração inicial falhar.

Posso comparar manualmente por conta própria e acompanhar o resultado:

String errorStr = "";
if(!obj1.getFieldA().equals(obj2.getFieldA())) {
    errorStr += "expected: " + obj1.getFieldA() + ", actual: " + obj2.getFieldA() + "\n";
}
if(!obj1.getFieldB().equals(obj2.getFieldB())) {
    errorStr += "expected: " + obj1.getFieldB() + ", actual: " + obj2.getFieldB() + "\n";
}
...
assertEquals("", errorStr);

Isso me dá uma imagem completa da igualdade, mas é desajeitado (e eu nem mesmo considerei os possíveis problemas nulos). Uma terceira opção é usar Comparator, mas compareTo () não me dirá quais campos falharam na igualdade.

Existe uma prática melhor para obter o que desejo do objeto, sem subclassificar e substituir equals (ugh)?

Ryan Nelson
fonte
Você está procurando uma biblioteca que faça comparações profundas para você? como deep-equals sugerido em stackoverflow.com/questions/1449001/… ?
Vikdor
3
Por que você precisa saber por que as duas instâncias não eram iguais. Normalmente, uma implementação de equalmétodo apenas informa se duas instâncias são iguais e não nos importamos por que as intâncias não são iguais.
Bhesh Gurung
3
Quero saber quais propriedades são desiguais para poder corrigi-las. :)
Ryan Nelson
Todos os Objects têm um equalsmétodo, você provavelmente não quis dizer nenhum método igual a substituído.
Steve Kuo
A melhor maneira que posso pensar é usar uma classe de wrapper ou uma subclasse e depois usá-la após substituir o método equals.
Thihara

Respostas:

66

Mockito oferece um combinador de reflexão:

Para a versão mais recente do Mockito, use:

Assert.assertTrue(new ReflectionEquals(expected, excludeFields).matches(actual));

Para versões mais antigas, use:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));
Felvhage
fonte
17
Esta aula está no pacote org.mockito.internal.matchers.apachecommons. Mockito docs declara: org.mockito.internal-> "Classes internas, não devem ser usadas por clientes." Você vai colocar seu projeto em risco usando isso. Isso pode mudar em qualquer versão do Mockito. Leia aqui: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac
6
Use em seu Mockito.refEq()lugar.
Jeremy Kao
1
Mockito.refEq()falha quando os objetos não têm um id definido = (
cavpollo
1
@PiotrAleksanderChmielowski, desculpe, ao trabalhar com Spring + JPA + Entities, o objeto Entity pode ter um id (representando o campo id da tabela do banco de dados), então quando ele está vazio (um novo objeto ainda não armazenado no DB), refEqfalha para comparar, pois o método hashcode não é capaz de comparar os objetos.
cavpollo de
3
Funciona bem, mas o esperado e o real estão na ordem errada. Deveria ser o contrário.
pkawiak 01 de
48

Existem muitas respostas corretas aqui, mas eu gostaria de adicionar minha versão também. Isso é baseado em Assertj.

import static org.assertj.core.api.Assertions.assertThat;

public class TestClass {

    public void test() {
        // do the actual test
        assertThat(actualObject)
            .isEqualToComparingFieldByFieldRecursively(expectedObject);
    }
}

ATUALIZAÇÃO: No assertj v3.13.2, este método está obsoleto, conforme apontado por Woodz em comentário. A recomendação atual é

public class TestClass {

    public void test() {
        // do the actual test
        assertThat(actualObject)
            .usingRecursiveComparison()
            .isEqualTo(expectedObject);
    }

}
Thomas
fonte
3
Em assertj v3.13.2, este método está obsoleto e a recomendação agora é para usar usingRecursiveComparison()com isEqualTo(), de modo que a linha éassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz
45

Eu geralmente implemento este caso de uso usando org.apache.commons.lang3.builder.EqualsBuilder

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));
Abhijeet Kushe
fonte
1
Gradle: androidTestCompile 'org.apache.commons: commons-lang3: 3.5'
Roel
1
Você precisa adicionar isso ao seu arquivo gradle em "dependências" quando quiser usar "org.apache.commons.lang3.builder.EqualsBuilder"
Roel
3
Isso não dá nenhuma dica sobre quais campos exatos não correspondem na verdade.
Vadzim
1
@Vadzim Eu usei o código abaixo para obter esse Assert.assertEquals (ReflectionToStringBuilder.toString (esperado), ReflectionToStringBuilder.toString (real));
Abhijeet Kushe
2
Este requer que todos os nós no gráfico implementem "igual" e "código hash", o que basicamente torna esse método quase inútil. O isEqualToComparingFieldByFieldRecursively de AssertJ é o que está funcionando perfeitamente no meu caso.
John Zhang
12

Eu sei que é um pouco velho, mas espero que ajude.

Encontro o mesmo problema que você, então, após investigação, encontrei poucas perguntas semelhantes a esta, e, depois de encontrar a solução, estou respondendo o mesmo nessas, pois achei que poderia ajudar outras pessoas.

A resposta mais votada (não aquela escolhida pelo autor) para esta pergunta semelhante , é a solução mais adequada para você.

Basicamente, consiste em usar a biblioteca chamada Unitils .

Este é o uso:

User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);

O que passará mesmo que a classe Usernão implemente equals(). Você pode ver mais exemplos e uma declaração muito legal chamada assertLenientEqualsem seu tutorial .

Lopezvit
fonte
1
Infelizmente, Unitilsparece que morreu, consulte stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams
8

Você pode usar Apache commons lang ReflectionToStringBuilder

Você pode especificar os atributos que deseja testar um por um ou, melhor, excluir aqueles que não deseja:

String s = new ReflectionToStringBuilder(o, ToStringStyle.SHORT_PREFIX_STYLE)
                .setExcludeFieldNames(new String[] { "foo", "bar" }).toString()

Você então compara as duas strings normalmente. Para o ponto sobre a reflexão ser lenta, presumo que seja apenas para teste, então não deve ser tão importante.

Matthew Farwell
fonte
1
O benefício adicional dessa abordagem é que você obtém uma saída visual exibindo os valores esperados e reais como strings, excluindo campos que você não se importa.
fquinner
7

Se você estiver usando o hamcrest para suas declarações (assertThat) e não quiser puxar libs de teste adicionais, você pode usar SamePropertyValuesAs.samePropertyValuesAspara declarar itens que não têm um método igual a sobrescrito.

A vantagem é que você não precisa puxar mais um framework de teste e ele apresentará um erro útil quando o assert falhar ( expected: field=<value> but was field=<something else>) em vez de expected: true but was falsese você usar algo como EqualsBuilder.reflectionEquals().

A desvantagem é que é uma comparação superficial e não há opção para excluir campos (como no EqualsBuilder), então você terá que contornar objetos aninhados (por exemplo, removê-los e compará-los independentemente).

Melhor caso:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
assertThat(actual, is(samePropertyValuesAs(expected)));

Caso feio:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
SomeClass expected = buildExpected(); 
SomeClass actual = sut.doSomething();

assertThat(actual.getSubObject(), is(samePropertyValuesAs(expected.getSubObject())));    
expected.setSubObject(null);
actual.setSubObject(null);

assertThat(actual, is(samePropertyValuesAs(expected)));

Então, escolha seu veneno. Estrutura adicional (por exemplo, Unitils), erro inútil (por exemplo, EqualsBuilder) ou comparação superficial (hamcrest).

Letreiro
fonte
1
Para trabalhar SamePropertyValuesAs, você deve adicionar no projeto dependecy hamcrest.org/JavaHamcrest/…
bigspawn
Eu gosto dessa solução, pois ela não adiciona uma "dependência adicional completamente * diferente!
GhostCat
2

Alguns dos métodos de comparação de reflexão são superficiais

Outra opção é converter o objeto em um json e comparar as strings.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;    
public static String getJsonString(Object obj) {
 try {
    ObjectMapper objectMapper = new ObjectMapper();
    return bjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
     } catch (JsonProcessingException e) {
        LOGGER.error("Error parsing log entry", e);
        return null;
    }
}
...
assertEquals(getJsonString(MyexpectedObject), getJsonString(MyActualObject))
Avraham Shalev
fonte
2

Usando o Shazamcrest , você pode fazer:

assertThat(obj1, sameBeanAs(obj2));
Leonel Sanches da Silva
fonte
Por que usar shazamcrest em vez de hamcrest? stackoverflow.com/a/27817702/6648326
MasterJoe
1
@ MasterJoe2 Em meus testes, reflectEqualsretorna falsequando os objetos possuem referências diferentes.
Leonel Sanches da Silva
1

Compare campo por campo:

assertNotNull("Object 1 is null", obj1);
assertNotNull("Object 2 is null", obj2);
assertEquals("Field A differs", obj1.getFieldA(), obj2.getFieldA());
assertEquals("Field B differs", obj1.getFieldB(), obj2.getFieldB());
...
assertEquals("Objects are not equal.", obj1, obj2);
EthanB
fonte
1
Isso é algo que eu não quero fazer, porque uma falha de declaração inicial ocultará as possíveis falhas abaixo.
Ryan Nelson
2
Desculpe, perdi essa parte da sua postagem ... Por que uma "imagem de igualdade total" é importante em um ambiente de teste de unidade? Os campos são todos iguais (aprovação no teste) ou nem todos são iguais (falha no teste).
EthanB
Não quero ter que executar novamente o teste para descobrir se outros campos não são iguais. Eu quero saber antecipadamente todos os campos que são desiguais para que eu possa abordá-los de uma vez.
Ryan Nelson
1
Afirmar muitos campos em um teste não seria considerado um verdadeiro teste de 'unidade'. Com o desenvolvimento orientado a teste tradicional (TDD), você escreve um pequeno teste e, em seguida, apenas código suficiente para fazê-lo passar. Ter uma declaração por campo é a maneira correta de fazer isso, mas não coloque todas as declarações em um único teste. Crie um teste diferente para cada afirmação de campo de seu interesse. Isso permitirá que você veja todos os erros em todos os campos com uma única execução da suíte. Se isso for difícil, provavelmente significa que seu código não é modular o suficiente em primeiro lugar e provavelmente pode ser refatorado em uma solução mais limpa.
Jesse Webb
Esta é definitivamente uma sugestão válida e é a maneira usual de resolver várias afirmações em um único teste. O único desafio aqui é, eu gostaria de uma visão holística do objeto. Ou seja, gostaria de testar todos os campos simultaneamente para verificar se o objeto está em um estado válido. Isso não é difícil de fazer quando você tem um método equals () sobrescrito (que é claro que eu não tenho neste exemplo).
Ryan Nelson
1

As asserções AssertJ podem ser usadas para comparar os valores sem o #equalsmétodo devidamente substituído, por exemplo:

import static org.assertj.core.api.Assertions.assertThat; 

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);
Pavel
fonte
1

Como essa pergunta é antiga, vou sugerir outra abordagem moderna usando JUnit 5.

Não gosto dessa solução porque não consigo entender o quadro completo da igualdade se uma declaração inicial falhar.

Com o JUnit 5, há um método chamado Assertions.assertAll()que permitirá agrupar todas as asserções em seu teste e executará cada uma e produzirá quaisquer asserções com falha no final. Isso significa que qualquer asserção que falhe primeiro não interromperá a execução das afirmações posteriores.

assertAll("Test obj1 with obj2 equality",
    () -> assertEquals(obj1.getFieldA(), obj2.getFieldA()),
    () -> assertEquals(obj1.getFieldB(), obj2.getFieldB()),
    () -> assertEquals(obj1.getFieldC(), obj2.getFieldC()));
patelb
fonte
0

Você pode usar a reflexão para "automatizar" o teste de igualdade completo. você pode implementar o código de "rastreamento" de igualdade que escreveu para um único campo e, em seguida, usar reflexão para executar esse teste em todos os campos do objeto.

Jtahlborn
fonte
0

Este é um método de comparação genérico, que compara dois objetos de uma mesma classe por seus valores de seus campos (tenha em mente que eles são acessíveis pelo método get)

public static <T> void compare(T a, T b) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    AssertionError error = null;
    Class A = a.getClass();
    Class B = a.getClass();
    for (Method mA : A.getDeclaredMethods()) {
        if (mA.getName().startsWith("get")) {
            Method mB = B.getMethod(mA.getName(),null );
            try {
                Assert.assertEquals("Not Matched = ",mA.invoke(a),mB.invoke(b));
            }catch (AssertionError e){
                if(error==null){
                    error = new AssertionError(e);
                }
                else {
                    error.addSuppressed(e);
                }
            }
        }
    }
    if(error!=null){
        throw error ;
    }
}
Shantonu
fonte
0

Me deparei com um caso muito semelhante.

Eu queria comparar em um teste que um objeto tinha os mesmos valores de atributos como outro, mas métodos, como is(), refEq(), etc não iria funcionar por razões como meu objeto com um valor nulo em seu idatributo.

Então esta foi a solução que encontrei (bem, um colega de trabalho encontrou):

import static org.apache.commons.lang.builder.CompareToBuilder.reflectionCompare;

assertThat(reflectionCompare(expectedObject, actualObject, new String[]{"fields","to","be","excluded"}), is(0));

Se o valor obtido de reflectionComparefor 0, significa que são iguais. Se for -1 ou 1, eles diferem em alguns atributos.

Cavpollo
fonte
0

Eu tive exatamente o mesmo enigma ao testar a unidade de um aplicativo Android, e a solução mais fácil que encontrei foi simplesmente usar o Gson para converter meus objetos de valor real e esperado jsone compará-los como strings.

String actual = new Gson().toJson( myObj.getValues() );
String expected = new Gson().toJson( new MyValues(true,1) );

assertEquals(expected, actual);

As vantagens disso sobre a comparação manual de campo por campo é que você compara todos os seus campos, portanto, mesmo se mais tarde adicionar um novo campo à sua classe, ele será testado automaticamente, em comparação a se você estivesse usando um monte deassertEquals() on todos os campos, que precisariam ser atualizados se você adicionar mais campos à sua classe.

jUnit também exibe as strings para que você possa ver diretamente onde elas diferem. Não tenho certeza de quão confiável é o ordenamento de campo Gson, porém, isso pode ser um problema potencial.

Magnus W
fonte
1
A ordem dos Fields não é garantida pela Gson. Você pode querer JsonParse as strings e comparar os JsonElements resultantes da análise
ozma
0

Tentei todas as respostas e nada realmente funcionou para mim.

Então, criei meu próprio método que compara objetos java simples sem ir fundo em estruturas aninhadas ...

O método retorna nulo se todos os campos corresponderem ou string contendo detalhes de incompatibilidade.

Apenas as propriedades que possuem um método getter são comparadas.

Como usar

        assertNull(TestUtils.diff(obj1,obj2,ignore_field1, ignore_field2));

Saída de amostra se houver uma incompatibilidade

A saída mostra os nomes das propriedades e respectivos valores dos objetos comparados

alert_id(1:2), city(Moscow:London)

Código (Java 8 e superior):

 public static String diff(Object x1, Object x2, String ... ignored) throws Exception{
        final StringBuilder response = new StringBuilder();
        for (Method m:Arrays.stream(x1.getClass().getMethods()).filter(m->m.getName().startsWith("get")
        && m.getParameterCount()==0).collect(toList())){

            final String field = m.getName().substring(3).toLowerCase();
            if (Arrays.stream(ignored).map(x->x.toLowerCase()).noneMatch(ignoredField->ignoredField.equals(field))){
                Object v1 = m.invoke(x1);
                Object v2 = m.invoke(x2);
                if ( (v1!=null && !v1.equals(v2)) || (v2!=null && !v2.equals(v1))){
                    response.append(field).append("(").append(v1).append(":").append(v2).append(")").append(", ");
                }
            }
        }
        return response.length()==0?null:response.substring(0,response.length()-2);
    }
Stan Sokolov
fonte
0

Como alternativa para junit apenas, você pode definir os campos como nulos antes da asserção igual a:

    actual.setCreatedDate(null); // excludes date assertion
    expected.setCreatedDate(null);

    assertEquals(expected, actual);
Andrew Taran
fonte
-1

Você pode colocar o código de comparação postado em algum método de utilitário estático?

public static String findDifference(Type obj1, Type obj2) {
    String difference = "";
    if (obj1.getFieldA() == null && obj2.getFieldA() != null
            || !obj1.getFieldA().equals(obj2.getFieldA())) {
        difference += "Difference at field A:" + "obj1 - "
                + obj1.getFieldA() + ", obj2 - " + obj2.getFieldA();
    }
    if (obj1.getFieldB() == null && obj2.getFieldB() != null
            || !obj1.getFieldB().equals(obj2.getFieldB())) {
        difference += "Difference at field B:" + "obj1 - "
                + obj1.getFieldB() + ", obj2 - " + obj2.getFieldB();
        // (...)
    }
    return difference;
}

Então, você pode usar este método no JUnit assim:

assertEquals ("Objetos não são iguais", "", findDifferences (obj1, obj));

que não é desajeitado e fornece informações completas sobre as diferenças, se elas existirem (embora não seja exatamente na forma normal de assertEqual, mas você obtém todas as informações, então deve ser bom).

Kamil
fonte
-1

Isso não vai ajudar o OP, mas pode ajudar qualquer desenvolvedor C # que acabe aqui ...

Como Enrique postou , você deve substituir o método equals.

Existe uma prática melhor para obter o que desejo do objeto, sem subclassificar e substituir equals (ugh)?

Minha sugestão é não usar uma subclasse. Use uma aula parcial.

Definições de classe parcial (MSDN)

Então, sua aula ficaria como ...

public partial class TheClass
{
    public override bool Equals(Object obj)
    {
        // your implementation here
    }
}

Para Java, eu concordaria com a sugestão de usar reflexão. Lembre-se de que você deve evitar o uso de reflexos sempre que possível. É lento, difícil de depurar e ainda mais difícil de manter no futuro porque os IDEs podem quebrar seu código fazendo uma renomeação de campo ou algo parecido. Seja cuidadoso!

Jesse Webb
fonte
-1

De seus comentários a outras respostas, não entendo o que você quer.

Apenas para fins de discussão, digamos que a classe substituiu o método equals.

Portanto, seu UT será parecido com:

SomeType expected = // bla
SomeType actual = // bli

Assert.assertEquals(expected, actual). 

E você está pronto. Além disso, você não pode obter a "imagem de igualdade total" se a afirmação falhar.

Pelo que entendi, você está dizendo que mesmo se o tipo substituísse igual, você não estaria interessado nele, pois deseja obter a "imagem de igualdade total". Portanto, também não há nenhum ponto em estender e substituir iguais.

Portanto, você tem opções: comparar propriedade por propriedade, usando reflexão ou verificações embutidas em código, eu sugeriria o último. Ou: compare representações legíveis por humanos desses objetos.

Por exemplo, você pode criar uma classe auxiliar que serializa o tipo que deseja comparar a um documento XML e, em seguida, compare o XML resultante! neste caso, você pode ver visualmente o que exatamente é igual e o que não é.

Essa abordagem lhe dará a oportunidade de ver a imagem completa, mas também é relativamente complicada (e um pouco sujeita a erros no início).

Vitaliy
fonte
É possível que meu termo "imagem de igualdade total" seja confuso. Implementar equals () realmente resolveria o problema. Estou interessado em saber todos os campos desiguais (relevantes para igualdade) ao mesmo tempo, sem ter que refazer o teste. Serializar o objeto é outra possibilidade, mas não preciso necessariamente de um equals profundo. Gostaria de utilizar as implementações equals () das propriedades, se possível.
Ryan Nelson
Ótimo! Você absolutamente pode utilizar os iguais das propriedades, conforme afirmou em sua pergunta. Parece que essa é a solução mais direta neste caso, mas como você observou, o código pode ser muito desagradável.
Vitaliy
-3

Você pode substituir o método equals da classe como:

@Override
public int hashCode() {
    int hash = 0;
    hash += (app != null ? app.hashCode() : 0);
    return hash;
}

@Override
public boolean equals(Object object) {
    HubRule other = (HubRule) object;

    if (this.app.equals(other.app)) {
        boolean operatorHubList = false;

        if (other.operator != null ? this.operator != null ? this.operator
                .equals(other.operator) : false : true) {
            operatorHubList = true;
        }

        if (operatorHubList) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

Bem, se você quiser comparar dois objetos de uma classe, você deve implementar de alguma forma o equals e o método de código hash

Enrique San Martín
fonte
3
mas OP diz que não quer substituir iguais, ele quer uma maneira melhor
Ankur