Na samym początku chcę zaznaczyć, że prawdopodobnie ten post nie będzie udzielał jednoznacznej odpowiedzi, jego celem (przynajmniej na chwilę obecną) jest zebranie wszystkich za i przeciw stosowaniu częściowego mockowania.
Zakładam, że ewentualnemu czytelnikowi sama idea mockowania jest znana. Partial mock danego obiektu to taka hybryda prawdziwego obiektu i mocka, innymi słowy część metod danego obiektu może być zmockowana, a cześć działać normalnie. Więcej o tym:
Co to daje? Przede wszystkim ułatwia testowanie, czy raczej samo napisanie testu. Wyobraźmy sobie sytuację, w której dana klasa ma jedną publiczną metodę, która korzysta z 20 innych prywatnych, pozagnieżdżanych między sobą. Załóżmy, że te 20 metod tworzy spójną całość i wydzielanie ich do osobnych klas byłoby bezsensowne. Napisanie testu dla takiej publicznej metody jest zajęciem dość karkołomnym. Taki test, który np. sprawdzałby czy metoda 17 robi coś tam poprawnie, zawiera w sobie całą masę mocków i innych obiektów, które umożliwiają taki przepływ w kodzie klasy, aby dojść do tej metody 17 i ją przetestować. Zrozumienie takiego testu dla człowieka, który go nie pisał jest dramatem.
Czy nie lepiej napisać oddzielny test dla metody 17? A potem odpowiednio sprawdzić, czy metoda ta jest wywoływana w innej metodzie, właśnie za pomocą partial mockingu? Jak dla mnie takie podejście jest o wiele przejrzystsze.
Konserwatywni w dziedzinie testowania, na pewno już się gotują, że piszę o testowaniu metod prywatnych, ale powiedzmy, że zmieniam im dostęp na pakietowy:). Tak, czy inaczej na pewno nie pochwalą partial mocków, bo to podejście śmierdzi, jest "niekoszerne".
Ogólnie zgadzam się z nimi w jednej kwestii, na pewno konieczność zastosowanie partial mockingu do testów, jest bardzo wyraźnym sygnałem, że z naszym kodem jest coś nie tak i prawdopodobnie lepszym wyjściem będzie jego refaktoring.
Jednakże istnieją sytuacje (jak chociażby legacy code), gdzie partial mock jest nie dość, że wygodnym, to czytelnym rozwiązaniem.
Oczywiście nie jestem w dziedzinie testów aż takim ekspertem jakbym chciał, dlatego jeśli istnieją inne przesłanki za lub przeciw partial mockom, to proszę o komentarze.
Artykuły o partial mocking:
http://nathan.crause.name/entries/programming/partial-mocking-stubbing-and-why-it-s-not-evil
http://monkeyisland.pl/2009/01/13/subclass-and-override-vs-partial-mocking-vs-refactoring/
http://stackoverflow.com/questions/3806977/partial-mocking-as-code-smell
"In theory, there is no difference between theory and practice. But, in practice, there is." Jan L. A. van de Snepscheut
wtorek, 21 czerwca 2011
poniedziałek, 18 kwietnia 2011
Zagadka java Date()
Jaki będzie wynik poniższego kodu:
System.out.println(new Date().getTime() < new Date().getTime());
piątek, 8 kwietnia 2011
Equals, hashCode i Hibernate
Ok, to był dość długi wstęp (1, 2, 3), może czasami jak dla dzieci, ale przynajmniej wszystko jest w 1 miejscu, a składa się na meritum problemu, czyli jak powinno się implementować metody equals() i hashCode() w połączeniu z Hibernatem. Mówiąc jeszcze bardziej precyzyjnie, czy możną używać pola reprezentującego klucz główny tabeli w tych metodach.
Najpierw trochę literatury:
http://community.jboss.org/wiki/EqualsandHashCode
https://forum.hibernate.org/viewtopic.php?t=928172
http://onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html?page=2
Wnioski z tego, z czym się do tej pory zapoznałem, są takie, że Hibernate mówi: nie używajcie ID, ale jak już musicie, to przynajmniej upewnijcie się, że obiekty (które będą porównywane) są w tej samej sesji! Dlatego proponuję otwartą dyskusję na ten temat. Obecnie przyjąłem sobie następujący schemat pisania equals() i hashCode(). W equals() biorę pod uwagę tylko id jeśli oba obiekty mają ustawione id (w moim przypadku różne od 0). W przeciwnym wypadku porównuję odpowiednie pola. W hashCode() nie wykorzystuje id, ponieważ może dojść do sytuacji, w której 2 obiekty takie same, będą miały różne hashe.
Najpierw trochę literatury:
http://community.jboss.org/wiki/EqualsandHashCode
https://forum.hibernate.org/viewtopic.php?t=928172
http://onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html?page=2
Wnioski z tego, z czym się do tej pory zapoznałem, są takie, że Hibernate mówi: nie używajcie ID, ale jak już musicie, to przynajmniej upewnijcie się, że obiekty (które będą porównywane) są w tej samej sesji! Dlatego proponuję otwartą dyskusję na ten temat. Obecnie przyjąłem sobie następujący schemat pisania equals() i hashCode(). W equals() biorę pod uwagę tylko id jeśli oba obiekty mają ustawione id (w moim przypadku różne od 0). W przeciwnym wypadku porównuję odpowiednie pola. W hashCode() nie wykorzystuje id, ponieważ może dojść do sytuacji, w której 2 obiekty takie same, będą miały różne hashe.
@Override
public boolean equals(Object object) {
if (this == object) return true;
if ( !(object instanceof Entity) ) return false;
Entity entity = (Entity) object;
if (id != 0 && entity.getId()!=0){
return id == entity.getId();
}
return new EqualsBuilder().append(field, entity.getField())
...//i inne pola
.isEquals();
}
poniedziałek, 28 marca 2011
Testowanie equals i hashCode
Zgodnie z obecną modą na wszechobecne testy jednostkowe wypadałoby jakoś testować nasze implementacje equals() i hashCode(), żeby się przy tym zbytnio nie narobić proponuję następujące rozwiązanie: klasa abstrakcycja, która sprawdza podstawowe warunki konktraktu dla equals() i hashCode() na podstawie 3 dostarczonych przez klasę dziedziczącą obiektów, które powinnym być identyczne wg. equals (ale maksymalnie od siebie różne) oraz jednego, który jest różny od pozastałych wg. equals. Pełny kod źródłowy wraz z klasą abstrakcyjną można pobrać korzystając z tego linku. Zastosowanie najlepiej widać na przykładzie:
Klasa testowana:
Klasa testująca:
Klasa testowana:
public class Entity {
private int field1;
private int field2;
private int notSignificantField;
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
public Entity(int field1, int field2, int notSignificantField) {
this.field1 = field1;
this.field2 = field2;
this.notSignificantField = notSignificantField;
}
...
}
Klasa testująca:
public class EntityTest extends AbstractEqualsTest<Entity> {
@Override
public Entity objectThatShouldBeEqualA() {
return new Entity(1, 2, 33);
}
@Override
public Entity objectThatShouldBeEqualB() {
return new Entity(1, 2, 44);
}
@Override
public Entity objectThatShouldBeEqualC() {
return new Entity(1, 2, 55);
}
@Override
public Entity objectThatShouldNotBeEqual() {
return new Entity(1, 1, 33);
}
}
czwartek, 3 marca 2011
Metoda hashCode w javie
Nudów ciąg dalszy, czyli kilka słów o metodzie hashCode(), która powinna być zawsze nadpisana, jeśli nadpisaliśmy metodą equals().
1. Kontrakt dla metody hashCode().
Metoda musi spełniać następujące warunki:
1. Kontrakt dla metody hashCode().
Metoda musi spełniać następujące warunki:
- W czasie działania aplikacji metoda zawsze zwraca tą samą wartość int, dla tego samego obiektu.
- Jeśli dwa obiekty są równe wg. metody equals() to wartości zwracane przez hashCode() dla tych obiektów muszą być takie same.
- Jeśli dwa obiekty nie są równe wg. metody equals() to wartości zwracane przez hashCode() dla tych obiektów nie muszą być takie same. Aczkolwiek zaleca się taką implementację hashCode(), żeby dla różnych obiektów zwracała różne wartości hash, co wpływa pozytywnie na szybkość operacji na hashowalnych kolekcjach.
@Override
public int hashCode() {
return new HashCodeBuilder().append(someField)
.append(someField2)
.toHashCode();
}
środa, 2 marca 2011
Zagadka
Jak ten młody leszcz dałem się ostatnio nabrać na TreeSet w Javie. Oto zagadka:
chciałem sobie posortować liście na podstawie ich koloru, użyłem TreeSet'a, jaki będzie wynik funkcji shouldTestTreeSet(), jaka będzie kolejność liści?
chciałem sobie posortować liście na podstawie ich koloru, użyłem TreeSet'a, jaki będzie wynik funkcji shouldTestTreeSet(), jaka będzie kolejność liści?
class Leaf{
int color;
int size;
public Leaf(int color, int size) {
this.color = color;
this.size = size;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof Leaf)) return false;
Leaf leaf = (Leaf) obj;
return new EqualsBuilder().append(this.color, leaf.color)
.append(this.size, leaf.size).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(color).append(size).toHashCode();
}
@Override
public String toString() {
return String.valueOf(color + " " +size);
}
}
class LeafComparator implements Comparator<Leaf>{
@Override
public int compare(Leaf o1, Leaf o2) {
return Ints.compare(o1.color, o2.color);
}
}
public class TreeSetTest {
public void shouldTestTreeSet() {
Leaf leaf = new Leaf(1,1);
Leaf leaf2 = new Leaf(1,2);
Leaf leaf3 = new Leaf(1,3);
Set<Leaf> tree = new TreeSet<Leaf>(new LeafComparator());
tree.add(leaf3);
tree.add(leaf);
tree.add(leaf2);
System.out.println(tree);
}
}
czwartek, 24 lutego 2011
Metody equlas i hashCode w Javie.
Niestety, aby dojść do meritum problemu, muszę część rzeczy powielić po raz kolejny, żeby nie szukać tego po innych stronach. Dopiero kolejny post będzie dotyczył konkretnie problemu equals i hashCode w kontekście użycia z Hibernatem.
Czyli, na początek, standardowo - jak powinno się zaimplementować metodę equals i hashCode.
1. Sygnatura metody.
Niby wszyscy wiedzą, że metoda eguals powinna zawsze wyglądać tak:
Reasumując, prawidłowa metoda equals ma sygnaturę:
2. Kontrakt dla metody equals().
Metoda musi spełniać następujące warunki:
Na pierwszy rzut oka wszystko jest ok, aczkolwiek złamany jest punkt drugi kontraktu, tj. symetryczność:
Niestety powyższy problem nie ma jednego dobrego rozwiązania. Taka jest natura obiektowo zorientowanego programowania. Mało tego nawet niektóre obiekty języka Java mają ten problem, np equals() z klasy Timestamp (która jest podklasą Date) łamie symetryczność.
Można to obejść np. zamiast dziedziczenia Point dodać prywatne pole typu Point do klasy ColorPoint, wtedy:
ważnym jest aby dla buildera wybrać tylko te pola, mają znaczenie przy porównaniu!
4. Jeśli nadpisujesz equals(), to zawsze nadpisuj hashCode()
ale o tym w następnym poście.
Czyli, na początek, standardowo - jak powinno się zaimplementować metodę equals i hashCode.
1. Sygnatura metody.
Niby wszyscy wiedzą, że metoda eguals powinna zawsze wyglądać tak:
public boolean equals(Object other)ale warto wspomnieć dlaczego np. nie można zastosować (dla obiektu Entity) czegoś takiego:
public boolean equals(Entity entity)problemy pojawiają się, np. przy użyciu kolekcji:
Entity entity = new Entity(1); Entity entity2 = new Entity(2); System.out.println(entity.equals(entity2)); //zwraca true Set<Entity> encje = new HashSet<Entity>(); encje.add(entity); System.out.println(encje.contains(entity2)); //zwraca falseostatnia linijka zwraca false, ponieważ źle nadpisaliśmy metodę equals() z klasy Object, która jako parametr przyjmuje Object. Dlatego nasza metoda to jedynie przeładowanie metody equals, a nie jej nadpisanie.
Reasumując, prawidłowa metoda equals ma sygnaturę:
@Override public boolean equals(Object other)i koniec kropka!!!!! Strażnikiem poprawności sygnatury jest adnotacja oczywiście @Override, jeśli kompilator nie warczy, że jest źle użyta, to z naszą sygnaturą jest wszystko OK (o ile nie dziedziczymy z jakiejś innej klasy...).
2. Kontrakt dla metody equals().
Metoda musi spełniać następujące warunki:
- zwrotność, czyli x.equals(x) == true
- symetryczność, czyli x.equals(y) == true, wtedy i tylko wtedy gdy y.equals(x) == true
- przechodniość, czyli jeśli x.equals(y)==true i y.equals(z)==true, wtedy x.equals(z)==true
- konsekwentność, czyli wielokrotne powtórzenie x.equals(y) zwraca zawsze tą wartość dla niezmienionych obiektów x i y.
- dla wartości null (np. x.equals(null)) metoda zawsze powinna zwracać false
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
//...
}
public class ColorPoint extends Point {
private Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
//łamie symetryczność
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return super.equals(o) && cp.color == color;
}
}
Na pierwszy rzut oka wszystko jest ok, aczkolwiek złamany jest punkt drugi kontraktu, tj. symetryczność:
Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED); p.equals(cp); //true cp.equals(p); //falsemożemy oczywiście poprawić metodę tak aby symetryczność została zachowana:
//łamie przechodniość
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// If o is a normal Point, do a color-blind comparison
if (!(o instanceof ColorPoint))
return o.equals(this);
// o is a ColorPoint; do a full comparison
ColorPoint cp = (ColorPoint) o;
return super.equals(o) && cp.color == color;
}
niestety, w tym przypadku łamana jest przechodniość:ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE); p1.equals(p2) //true p2.equals(p3) //true p1.equals(p3) //false
Niestety powyższy problem nie ma jednego dobrego rozwiązania. Taka jest natura obiektowo zorientowanego programowania. Mało tego nawet niektóre obiekty języka Java mają ten problem, np equals() z klasy Timestamp (która jest podklasą Date) łamie symetryczność.
Można to obejść np. zamiast dziedziczenia Point dodać prywatne pole typu Point do klasy ColorPoint, wtedy:
class ColorPoint {
private Point point;
private Color color;
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
3. Przykład jak zaimplementować equals().@Override
public boolean equals(Object object) {
if (this == object) return true; //załatwia punkt 1. kontraktu
if ( !(object instanceof Entity) ) return false; //dodatkowo załatwia punkt 5 kontraktu
Entity entity = (Entity) object;
//dla porównania konkretnych warto posłużyć się już gotowym builderem z biblioteki commons
return new EqualsBuilder().append(field1, entity.getField1())
.append(field2, entity.getField2())
.isEquals();
}
ważnym jest aby dla buildera wybrać tylko te pola, mają znaczenie przy porównaniu!
4. Jeśli nadpisujesz equals(), to zawsze nadpisuj hashCode()
ale o tym w następnym poście.
Subskrybuj:
Posty (Atom)