0

I have a few Management classes that are used for search methods, add, change and delete methods, print in table format method and write map to file method. The classes also have a container each as an attribute. Lets say there is a class X. This would be the class XManagement, and its container has objects of class X.

search() method returns the object of X, but first it gathers its ID via input.

add() method gathers input data for the creation of an object X, and the very last line of its code is for adding that object to its container.

change() method first searches for the object the user wants to change (via search() method), and then gathers data and changes the object via setter methods. It then calls the write() method for re-writing the file.

delete() method searches for the object (via search()), and then just removes it from its container, after which it calls the write() method.

The write() method is also void. It goes through the container for each object, and its data is then appended to a parse-able String, which is written to file.

Here are the examples:

public class XManagement {

    protected Hashtable<Integer, X> xes = new Hashtable<>();

    public XManagement(String fileName) {
        // Constructor.
        // Loads the input file, then parses it.
        // Once parsed, the objects of X class are created.
        // They are then put into the container (xes).
    }

    protected X search() {
        // Both generic methods.
        Integer uuid = enterInteger("ID");
        return (X) find(uuid, xes);
    }

    public void add() {
        Integer uuid = UUID(xes); // Generic method, generates UUID.hashCode() 
                                  // and checks for duplicates.
        String a = enterString("Name");
        Date d = enterDate("Start");
        // ...............
        X x = new X(uuid, a, d, etc);
        xes.put(x.getID(), x);
        write();
    }

    public void delete() {
        X x = search();
        xes.remove(x.getID(), x);
        write();
    }

    public void change() {
        X x = search();
        String a = enterString("Name");
        x.setA(a);
        Date d = enterDate("Start");
        x.setD(d);
        // .......................
        write();
    }

    protected void write() {
        File file = new File("x.txt");
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        String curr = "";
        for (int id : xes.keySet()) {
            curr += xes.get(id).getA() + "|" + xes.get(id).getD() + "|"; // etc
        }
        out.write(curr);
        // There's, naturally, try/catch/finally here. For the sake of simplicity, I left it out here.
    }
}

Class X goes like this:

public class X {
    String a;
    Date d;
    // etc
    public X(String a, Date d) {
        this.a = a;
        this.d = d;
    }
    // Getters and setters.
}

It's a lot more complicated than that, I just tried to keep it simple here to get some help - I'll try to figure out the harder stuff when I get the basics.

In some management classes, methods and constructors have the instances of other Management classes as their input parameters, so that they can call their methods inside, because most of them are connected. Let's say the Y class has X as an attribute, and when I create a Y object in YManagement add() method, I need to be able to choose one from all the available X objects from xes, via the search() method contained in XManagement.

I decided to keep it simple for now, but if you want, you can tell me how to approach testing where I'd have instances of other Management classes as an input.


How do I write detailed JUnit 5 test cases for these methods?

Sorry if I made a mistake somewhere in the code, I haven't copied it but written in here, generalizing the stuff that gets repeated in other Management classes.

If you have any other suggestions, as to the code itself, feel free to write that.

duffymo
  • 305,152
  • 44
  • 369
  • 561
  • 1
    Your question is too broad. What exactly is the problem here? A unit test is always basically the same thing: a method is supposed to do something, and the tests checks that it does indeed does that. So, if you have a method supposed to add a foo to a FooManagement and delete a bar from a BarManagement, then you call that method, and check that the foo has indeed been added, and the bar has indeed been removed. You *can* mock the FooManagement and the BarManagement if it makes things simpler. – JB Nizet Aug 15 '17 at 12:56
  • Thanks, what's mocking though? – Vladimir Gajčin Aug 15 '17 at 13:18
  • 1
    First link returned when googling for "what is mocking?": https://stackoverflow.com/questions/2665812/what-is-mocking – JB Nizet Aug 15 '17 at 13:20

3 Answers3

2

These methods are hard to test because they're doing too much. You have input, output to files, and data modifications.

Let's look at this method:

protected X search() {
    // Both generic methods.
    Integer uuid = enterInteger("ID");
    return (X) find(uuid, xes);
}

Why do you call enterInteger when you could pass the desired ID into the method as a parameter? Let the client tell your class which ID to search for. Now the search is doing one thing: looking up a reference in the map.

I think that naming a class X gives no information whatsoever about what it's for. I'd prefer something that gives me a hint - better readability. You abstract all information out of the code with this naming scheme. Good names matter. Think harder about this one.

Your XManagement class looks like a simplistic in-memory database. Have you thought about using something that would allow you to use SQL? Maybe H2 would be a better choice. If this class were interface based you could swap out the implementation and clients would not have to change.

A better design would partition responsibility out to separate classes. For example, your data object could be accompanied by an interface-based persistence tier that would handle searches, updates, persistence, etc.

When I find that methods are too hard to test, it's usually a sign that the class needs to be redesigned. Hard to test is the same thing as hard to use for clients.

I'd replace your XManagement class with an interface:

package persistence;

public interface Repository<K, V> {
    List<V> find();
    V find(K id);
    List<V> find(Predicate<V> filter); 
    void save(V v);
    void update(V v);
    void delete(K id);
    void delete(V v);
}

You'll have an instance for each one of your Shows, Performances, Tickets, Users, etc.

package persistence;

public class ShowRepository implements Repository<Integer, Show> {

    // TODO: You'll need a constructor and a Map for Shows.

    public List<Show> find() {  // the rest for you }
    public Show find(Integer id) {  // the rest for you }
    public List<Show> find(Predicate<Show> filter) {  // the rest for you }
    public void save(Show v) {  // the rest for you }
    public void update(Show v) {  // the rest for you }
    public void delete(Integer id) {  // the rest for you }
    public void delete(Show v) {  // the rest for you }    
}

Much better than your X, in my opinion.

If you write your class using my interface there won't be any console interaction in those classes. Everything it needs is passed in by callers.

You can create separate concrete implementations for an in-memory cache, a relational or NoSQL database that each implement this interface.

duffymo
  • 305,152
  • 44
  • 369
  • 561
  • Well the whole thing is for a project we were given to make. No libraries allowed. It's about a theater, with classes for Shows, Performances, Tickets, Users (Manager,Cashier?), Scenes etc. So that's that. I generalized the similar stuff that occurs in each of the classes. Furthermore, why would I need a method add(X x) which would only put it into the container - for 1 line of code? – Vladimir Gajčin Aug 15 '17 at 13:13
  • In the project, all classes and attributes are named accordingly, of course. – Vladimir Gajčin Aug 15 '17 at 13:15
  • enterInteger(String req) calls for Scanner input, but before that it prints what's necessary - the req parameter. So I have 1 method that allows me to enter Integers, each with a try/catch method etc. – Vladimir Gajčin Aug 15 '17 at 13:18
  • 2
    Wrong. You need to push that IO interaction out of the method. Do it outside and pass in the value. You tie this class to console applications when you write it this way. – duffymo Aug 15 '17 at 13:20
  • It's called "abstraction". You should have been taught about such things. If it's a student project, and you aren't interested in learning how to write better code, then why are you asking here? – duffymo Aug 15 '17 at 13:21
  • This IS a console application. – Vladimir Gajčin Aug 15 '17 at 13:25
  • I am actually interested in how to write better code. You should've seen this project few months ago, haha. – Vladimir Gajčin Aug 15 '17 at 13:29
  • I know that, but you should be thinking about pushing all the console specific code out to the edges. Don't embed user interaction in the main classes. Isolate it. – duffymo Aug 15 '17 at 13:31
  • Mind elaborating on that latest comment? – Vladimir Gajčin Aug 15 '17 at 13:56
  • Ok, I've done what you suggested. So, how do I test those few methods from the ShowRepository? – Vladimir Gajčin Aug 17 '17 at 16:16
1

You question is rather broad.
So, I will focus on the essential.

1) How to test void methods ?

A void method doesn't return any result but it creates side effect on the underlying object/system.
So you have to assert that the void method does what it is designed to do by asserting that the expected side effect is effective.
For example your add() method adds the object in the HashTable (you should rather use a HashMap or a ConcurrentHashMap if you have race conditions), so you should check that the object was correctly added.
You could for example have a search() method that return an object if it is contained. And by using it you could check if the object was added :

X x = ...;
xManagement.add(x);
X actualX = xManagement.search(x.getId());
assertEquals(x, actualX)

To do it, you have to make evolve your actual class that actually doesn't provide a simple retrieval method.

2) How to test classes that have dependencies with other classes ?

Unit tests of a class should be done in isolation of other classes.
So if YManagement methods have to invoke methods of XManagement, you should mock XManagement dependency and record a behavior for it.
Don't test twice the same thing.

davidxxx
  • 125,838
  • 23
  • 214
  • 215
1

You need to redesign your code as current implementation is untestable. I suggest following steps:

  • break your code to more cohesive classes;
  • extract interfaces;
  • use dependency injection for provided classes;
  • use parametrized methods;

After that you will be able to test your class with mocked dependencies or fake objects. Check out SOLID principles as if you follow them your code will be testable and maintanable.

Izbassar Tolegen
  • 1,990
  • 2
  • 20
  • 37