Java testy jednostkowe w JUnit - prośba o ocenę kodu

Java testy jednostkowe w JUnit - prośba o ocenę kodu
XE
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 4 lata
  • Postów:152
0

Witam forumowiczów,
pierwszy raz zabrałem się w życiu za pisanie testów jednostkowych i nie wiem czy robię to dobrze. Jakby ktoś kto się zna mógł zerknąć, doradzić co poprawić itp. to byłoby super ^^

Kopiuj
package pl.xezolpl.cwiczenia.basket;

public class Product {

    private String name;
    private double price;
    private int amount;
          ///GETTERS
    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    public int getAmount() {
        return amount;
    }
            ///SETTERS
    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }
             ///CONSTRUCTOR
    Product(String name, double price, int amount) {
        this.name = name;
        this.price = price;
        this.amount = amount;
    }
}

Kopiuj
package pl.xezolpl.cwiczenia.basket;

import java.text.DecimalFormat;
import java.util.*;

import pl.xezolpl.cwiczenia.basket.Product;

public class Basket {

    private Map<String, Product> productHashMap = new HashMap<>();

    public boolean addProductToBasket(Product product) {
        if ((product.getName() == "") || (product.getAmount() == 0) || (product.getPrice() == 0)) {
            return false;
        } else if (!productIsInTheBasket(product.getName())) {
            productHashMap.put(product.getName(), product);
            return true;
        } else if (productIsInTheBasket(product.getName())) {
            productHashMap.get(product.getName()).setAmount(
                    productHashMap.get(product.getName()).getAmount() + product.getAmount()); /// sum of amounts
            return true;
        }
        return false;
    }

    public boolean removeProductFromBasket(String productName) {
        if (productIsInTheBasket(productName)) {
            productHashMap.remove(productName);
            return true;
        }
        return false;
    }

    public boolean changeAmountOfProductinBasket(String productName, int newAmount) {
        if (productIsInTheBasket(productName)) {
            productHashMap.get(productName).setAmount(newAmount);
            return true;
        }
        return false;
    }

    private double calculateBasketContent() {
        double sum = 0.0;
        for (Map.Entry<String, Product> entry : productHashMap.entrySet()) {
            sum += entry.getValue().getAmount() * entry.getValue().getPrice(); /// sum += price*amount
        }
        return ((double) ((int) (sum*100)))/100; /// cut off to the 2 decimal places

    }

    public void display() {
        System.out.println("YOUR BASKET");
        System.out.println("---------------------------");
        System.out.println("Name      Price      Amount\n");
        for (Map.Entry<String, Product> entry : productHashMap.entrySet()) {
            System.out.println(entry.getValue().getName() + "      " + entry.getValue().getPrice() + "      "
                    + entry.getValue().getAmount());
        }
        System.out.println("----------------");
        System.out.println("Total price: " + calculateBasketContent());
    }

    private boolean productIsInTheBasket(String productName) {
        if (productHashMap.get(productName) != null) {
            return true;
        }
        return false;
    }
}

Kopiuj
package pl.xezolpl.cwiczenia.basket;

import org.junit.*;

public class Tests {

    private Product apple;
    private Basket basket;
    private static final double price = 0.01;

    @Before
    public void setUp() {
        apple = new Product("Apple", 2.99, 4);
        basket = new Basket();
    }

    @Test
    public void shouldAllowToAddProduct() {
        Assert.assertTrue(basket.addProductToBasket(apple));
    }

    @Test
    public void shouldDenyToAddBlankProduct() {
        Assert.assertFalse(basket.addProductToBasket(new Product("", 0, 0)));
    }

    @Test
    public void shouldAllowToAddTheSameItemTwice() {
        basket.addProductToBasket(apple);
        Assert.assertTrue(basket.addProductToBasket(apple));
        basket.display();
    }

    @Test
    public void shouldAllowToChangeProductWhatIsNotInTheBasket() {
        Assert.assertFalse(basket.changeAmountOfProductinBasket("Something", 3));
    }

    @Test
    public void shouldAllowToChangeProductAmount() {
        basket.addProductToBasket(apple);
        Assert.assertTrue(basket.changeAmountOfProductinBasket(apple.getName(), 7));
    }

    @Test
    public void shouldAllowToRemoveProductFromBasket() {
        basket.addProductToBasket(apple);
        Assert.assertTrue(basket.removeProductFromBasket(apple.getName()));
    }
    @Test
    public void shouldAllowToRemoveProductWhatIsNotInTheBasket(){
        Assert.assertFalse(basket.removeProductFromBasket("Something"));
    }
}

I czy w ogóle o to w tym chodzi, czy nie przesadziłem z if-ami w części basket-owej, bo mam takie wrażenie?


„Standardowa edukacja zapewni Ci przeżycie. Samokształcenie- fortunę." - Jim Rohn
edytowany 3x, ostatnio: Xezolpl
XE
Na strukturę katalogów nie patrzmy, wiem że powinno to być inaczej ale w ramach ćwiczenia może tak pozostać
Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:około 7 godzin
  • Postów:1880
1

Nie wyglada to źle :) fajnie, że testy sprawdzają tylko jedną rzecz. Kilka uwag:

  1. Tutaj masz dość prosty przykład, ale w ramach nauki możesz już teraz nadawać testom strukturę given-when-then. Zaprocentuje na przyszłość.
  2. Metoda „productIsInTheBasket” -> można uprościć do jednej linii.
  3. Klasa „Product” jest niepotrzebnie mutowalna. Powinieneś również rozdzielić „Produkt” jako towar od „Produktu” jako item w koszyku, który ma liczbę w ramach koszyka.
  4. Używaj static imports: „Assert.assertTrue” -> „assertTrue”. Polecam w ogóle jak najszybciej przejść na AssertJ lub Spock.
  5. W jednym teście masz „display()” - po co? Poza tym koszyk nie powinien umieć się sam wypisać na ekran - powinny być do tego dedykowane klasy jak np. ConsoleBasketDisplay czy JsonBasketDisplay - koszyk nie powinien mieć świadomości, jak jest wyświetlany. Oczywiście, możesz dodać po prostu „toString()” zamiast „display()”.

”Engineering is easy. People are hard.” Bill Coughran
edytowany 3x, ostatnio: Charles_Ray
XE
Mam pytanie w ramach given-when-then a mianowicie nie rozumiem oddzielania jakoś większego given od @Before
baant
bo ustawiasz tylko raz obiekt testowy, zawsze taki sam. Jak bedziesz w kazdym tescie potrzebowal inny Product to przygotowujesz se go w "given"
Charles_Ray
W „given” pokazujesz istotne założenia dla danego testu
XE
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 4 lata
  • Postów:152
0
Charles_Ray napisał(a):

Nie wyglada to źle :) fajnie, że testy sprawdzają tylko jedną rzecz. Kilka uwag:

  1. Tutaj masz dość prosty przykład, ale w ramach nauki możesz już teraz nadawać testom strukturę given-when-then. Zaprocentuje na przyszłość.
  2. Metoda „productIsInTheBasket” -> można uprościć do jednej linii.
  3. Klasa „Product” jest niepotrzebnie mutowalna. Powinieneś również rozdzielić „Produkt” jako towar od „Produktu” jako item w koszyku, który ma liczbę w ramach koszyka.
  4. Używaj static imports: „Assert.assertTrue” -> „assertTrue”. Polecam w ogóle jak najszybciej przejść na AssertJ lub Spock.
  5. W jednym teście masz „display()” - po co? Poza tym koszyk nie powinien umieć się sam wypisać na ekran - powinny być do tego dedykowane klasy jak np. ConsoleBasketDisplay czy JsonBasketDisplay - koszyk nie powinien mieć świadomości, jak jest wyświetlany. Oczywiście, możesz dodać po prostu „toString()” zamiast „display()”.

1.Doczytam, bo szczerze powiedziawszy pierwszy raz słyszę.
2.Hmmm coś takiego? return !productHashMap.get(productName).equals(null);
3.Nie rozumiem, chodzi ci o jakiś model przejściowy, komunikacyjny między tymi klasami czy co?
4.A no rzeczywiście, było na kursie ale zapomniałem użyć, racja, już zmieniam :)
5.A to tak poglądowo czy amounty się sumują. Hmm no niby tak, no powinno być, ale toString zamiast display to nie rozumiem, tylko o zmianę nazewnictwa ci chodzi czy jak?

I oczywiście dziękuję za odpowiedź, już poprawiam moje testy ;)


„Standardowa edukacja zapewni Ci przeżycie. Samokształcenie- fortunę." - Jim Rohn
Charles_Ray
Mapa ma metodę „containsKey” :) poczytaj tez o metodzie „merge”, będziesz mógł prawdopodobnie zapisać te cała ifologię jednym poleceniem. Jako lekturę obowiązkowa polecam https://leanpub.com/badtestsgoodtests
L2
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 5 lat
  • Postów:10
1

Mała kosmetyka, trochę poprawi czytelność.

Kopiuj
public boolean addProductToBasket(Product product) {

     if ((product.isXxx()) { // Xxx -  Null, Blank, Empty, ContainData, cokolwiek  
            return false;
     }

	if (!productIsInTheBasket(product.getName())) {
            productHashMap.put(product.getName(), product);
            return true;
     } 

	if (productIsInTheBasket(product.getName())) {
            productHashMap.get(product.getName()).setAmount(
                    productHashMap.get(product.getName()).getAmount() + product.getAmount()); /// sum of amounts
            return true;
     }

     return false;

    }

Nad tym bym popracował:

Kopiuj
productHashMap.get(product.getName()).setAmount(
                    productHashMap.get(product.getName()).getAmount() + product.getAmount()); /// sum of amounts

edytowany 1x, ostatnio: loki2000
baant
  • Rejestracja:ponad 11 lat
  • Ostatnio:3 miesiące
  • Lokalizacja:Wrocław
  • Postów:524
1
Kopiuj
if (productIsInTheBasket(product.getName())) {
   existing(???) = productHashMap.get(product.getName())
   existing.addAmount(product) 
   ....

Mniej kodu ale i tak jest to z dupeczki, bo niby czemu Product ma pole amount? Koszyk powinien mieć taką informacje

edytowany 1x, ostatnio: baant
XE
Hmmm, racja, rzeczywiście nie przemyślałem tego, dzięki :)
Shalom
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
1

if ((product.getName() == "") || (product.getAmount() == 0) || (product.getPrice() == 0)) {

A może po prostu walidować tworzenie Productu od razu? ;)

} else if (productIsInTheBasket(product.getName())) {

Po co drugi check? Skoro przed chwilą było że to false, to wystarczyłby else

public boolean removeProductFromBasket(String productName) {

Wiesz że remove z Map<K,V> robi dokładnie to samo? Po co tak zaimplementowałeś?

sum += entry.getValue().getAmount() * entry.getValue().getPrice();

Dramat! Czemu klasa Product nie potrafi zwrócić swojego kosztu tylko do liczysz jak zwierze wyciągając bebechy tego Producta?

return ((double) ((int) (sum*100)))/100; /// cut off to the 2 decimal places

Jeśli musisz dać taki komentarz to znaczy że należało z tego zrobić osobną metodę z ludzką nazwą.

public void display() {

A może zamiast tego metoda toString() ? ;)

private boolean productIsInTheBasket(String productName) {

jw z remove, java już to ma!


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
Charles_Ray
Ad. "Dramat!" - nie widzisz, że to osoba początkująca?
YA
  • Rejestracja:około 10 lat
  • Ostatnio:około 5 godzin
  • Postów:2370
0

Parę uwag, które nie są bezpośrednio związane z testami:

  • "amount" - w angielskim używane do określenia ilości czegoś niepodzielnego. W kontekście ilości produktów może po prostu "quantity" ?
  • operacje na pieniądzach - może cenę trzymać jako Money (JSR-354)?
  • getters/setters/constructors - komentarze, które wg mnie nic nie wnoszą - widać czym są wspomniane metody. Można rzecz jasna polemizować i mówić, że to "taka konwencja".

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.