Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Junit4 testing; protected methods

I'm working to test this chunk of code - it's a class called MazeBuilder. My problem is that most of the methods are protected, so I can't access them in the tests...

So my thought was that the test should just focus on Run(), so it accesses a lot of the other methods. But I'm concerned that it will be impossible to get any sort of cohesive testing done operating from just one method.

Additionally, what is the proper way to test the 2 constructors ( MazeBuilder() and MazeBuilder(boolean deterministic) )? As it stands, I'm just testing that the object formed is not null - i.e. that they're being built at all. Is there a more expansive way of testing a constructor that I'm unaware of?

package falstad;

public class MazeBuilder implements Runnable {
    // Given input information: 
    protected int width, height ;   // width and height of maze, 
    Maze maze;              // reference to the maze that is constructed, results are returned by calling maze.newMaze(..)
    private int rooms;      // requested number of rooms in maze, a room is an area with no walls larger than a single cell
    int expectedPartiters;  // user given limit for partiters

    // Produced output information to create the new maze
    // root, cells, dists, startx, starty
    protected int startx, starty ; // starting position inside maze for entity to search for exit
    // conventional encoding of maze as a 2 dimensional integer array encapsulated in the Cells class
    // a single integer entry can hold information on walls, borders/bounds
    protected Cells cells; // the internal representation of a maze as a matrix of cells
    protected Distance dists ; // distance matrix that stores how far each position is away from the exit positino

    // class internal local variables
    protected SingleRandom random ; // random number stream, used to make randomized decisions, e.g for direction to go

    Thread buildThread; // computations are performed in own separated thread with this.run()

    //int colchange; // randomly selected in run method of this thread, used as parameter to Segment class constructor

    /**
     * Constructor for a randomized maze generation
     */
    public MazeBuilder(){
        random = SingleRandom.getRandom();
    }
    /**
     * Constructor with option to make maze generation deterministic or random
     */
    public MazeBuilder(boolean deterministic){
        if (true == deterministic)
        {
            System.out.println("Project 2: functionality to make maze generation deterministic not implemented yet! Fix this!");
            // Control random number generation
            // TODO: implement code that makes sure that if MazeBuilder.build is called for same skill level twice, same results
            // HINT: check http://download.oracle.com/javase/6/docs/api/java/util/Random.html and file SingleRandom.java

        }
        random = SingleRandom.getRandom();
    }

    /**
     * Provides the sign of a given integer number
     * @param num
     * @return -1 if num < 0, 0 if num == 0, 1 if num > 0
     */
    static int getSign(int num) {
        return (num < 0) ? -1 : (num > 0) ? 1 : 0;
    }

    /**
     * This method generates a maze.
     * It computes distances, determines a start and exit position that are as far apart as possible. 
     */
    protected void generate() {
        // generate paths in cells such that there is one strongly connected component
        // i.e. between any two cells in the maze there is a path to get from one to the other
        // the search algorithm starts at some random point
        generatePathways(); 

        final int[] remote = dists.computeDistances(cells) ;

        // identify cell with the greatest distance
        final int[] pos = dists.getStartPosition();
        startx = pos[0] ;
        starty = pos[1] ;

        // make exit position at true exit in the cells data structure
        setExitPosition(remote[0], remote[1]);
    }

    /**
     * This method generates pathways into the maze.
     * 
     */
    protected void generatePathways() {
        int[][] origdirs = new int[width][height] ; 

        int x = random.nextIntWithinInterval(0, width-1) ;
        int y = 0; 
        final int firstx = x ; 
        final int firsty = y ;
        int dir = 0;        
        int origdir = dir;  
        cells.setVisitedFlagToZero(x, y); 
        while (true) {      

            int dx = Constants.DIRS_X[dir];
            int dy = Constants.DIRS_Y[dir];

            if (!cells.canGo(x, y, dx, dy)) { 

                dir = (dir+1) & 3; 
                if (origdir == dir) { 

                    if (x == firstx && y == firsty)
                        break; 


                    int odr = origdirs[x][y];
                    dx = Constants.DIRS_X[odr];
                    dy = Constants.DIRS_Y[odr];

                    x -= dx;
                    y -= dy;



                    origdir = dir = random.nextIntWithinInterval(0, 3);
                }
            } else {


                cells.deleteWall(x, y, dx, dy);

                x += dx;
                y += dy;

                cells.setVisitedFlagToZero(x, y);

                origdirs[x][y] = dir;
                origdir = dir = random.nextIntWithinInterval(0, 3);
            }
        }
    }
    /**
     * Establish valid exit position by breaking down wall to outside area.
     * @param remotex
     * @param remotey
     */
    protected void setExitPosition(int remotex, int remotey) {
        int bit = 0;
        if (remotex == 0)
            bit = Constants.CW_LEFT;
        else if (remotex == width-1)
            bit = Constants.CW_RIGHT;
        else if (remotey == 0)
            bit = Constants.CW_TOP;
        else if (remotey == height-1)
            bit = Constants.CW_BOT;
        else
            dbg("Generate 1");
        cells.setBitToZero(remotex, remotey, bit);
        //System.out.println("exit position set to zero: " + remotex + " " + remotey + " " + bit + ":" + cells.hasMaskedBitsFalse(remotex, remotey, bit)
        //      + ", Corner case: " + ((0 == remotex && 0 == remotey) || (0 == remotex &&  height-1 == remotey) || (width-1 == remotex && 0 == remotey) || (width-1 == remotex && height-1 == remotey)));
    }


    static final int MIN_ROOM_DIMENSION = 3 ;
    static final int MAX_ROOM_DIMENSION = 8 ;
    /**
     * Allocates space for a room of random dimensions in the maze.
     * The position of the room is chosen randomly. The method is not sophisticated 
     * such that the attempt may fail even if the maze has ample space to accommodate 
     * a room of the chosen size. 
     * @return true if room is successfully placed, false otherwise
     */
    private boolean placeRoom() {
        // get width and height of random size that are not too large
        // if too large return as a failed attempt
        final int rw = random.nextIntWithinInterval(MIN_ROOM_DIMENSION, MAX_ROOM_DIMENSION);
        if (rw >= width-4)
            return false;

        final int rh = random.nextIntWithinInterval(MIN_ROOM_DIMENSION, MAX_ROOM_DIMENSION);
        if (rh >= height-4)
            return false;

        // proceed for a given width and height
        // obtain a random position (rx,ry) such that room is located on as a rectangle with (rx,ry) and (rxl,ryl) as corner points
        // upper bound is chosen such that width and height of room fits maze area.
        final int rx = random.nextIntWithinInterval(1, width-rw-1);
        final int ry = random.nextIntWithinInterval(1, height-rh-1);
        final int rxl = rx+rw-1;
        final int ryl = ry+rh-1;
        // check all cells in this area if they already belong to a room
        // if this is the case, return false for a failed attempt
        if (cells.areaOverlapsWithRoom(rx, ry, rxl, ryl))
            return false ;
        // since the area is available, mark it for this room and remove all walls
        // from this on it is clear that we can place the room on the maze
        cells.markAreaAsRoom(rw, rh, rx, ry, rxl, ryl); 
        return true;
    }

    static void dbg(String str) {
        System.out.println("MazeBuilder: "+str);
    }



    /**
     * Fill the given maze object with a newly computed maze according to parameter settings
     * @param mz maze to be filled
     * @param w width of requested maze
     * @param h height of requested maze
     * @param roomct number of rooms
     * @param pc number of expected partiters
     */
    public void build(Maze mz, int w, int h, int roomct, int pc) {
        init(mz, w, h, roomct, pc);
        buildThread = new Thread(this);
        buildThread.start();
    }

    /**
     * Initialize internal attributes, method is called by build() when input parameters are provided
     * @param mz maze to be filled
     * @param w width of requested maze
     * @param h height of requested maze
     * @param roomct number of rooms
     * @param pc number of expected partiters
     */
    private void init(Maze mz, int w, int h, int roomct, int pc) {
        // store parameters
        maze = mz;
        width = w;
        height = h;
        rooms = roomct;
        expectedPartiters = pc;
        // initialize data structures
        cells = new Cells(w,h) ;
        dists = new Distance(w,h) ;
        //colchange = random.nextIntWithinInterval(0, 255); // used in the constructor for Segments  class Seg
    }

    static final long SLEEP_INTERVAL = 100 ; //unit is millisecond
    /**
     * Main method to run construction of a new maze with a MazeBuilder in a thread of its own.
     * This method is called internally by the build method when it sets up and starts a new thread for this object.
     */
    public void run() {
        // try-catch block to recognize if thread is interrupted
        try {
            // create an initial invalid maze where all walls and borders are up
            cells.initialize();
            // place rooms in maze
            generateRooms();

            Thread.sleep(SLEEP_INTERVAL) ; // test if thread has been interrupted, i.e. notified to stop

            // put pathways into the maze, determine its starting and end position and calculate distances
            generate();

            Thread.sleep(SLEEP_INTERVAL) ; // test if thread has been interrupted, i.e. notified to stop

            final int colchange = random.nextIntWithinInterval(0, 255); // used in the constructor for Segments  class Seg
            final BSPBuilder b = new BSPBuilder(maze, dists, cells, width, height, colchange, expectedPartiters) ;
            BSPNode root = b.generateBSPNodes();

            Thread.sleep(SLEEP_INTERVAL) ; // test if thread has been interrupted, i.e. notified to stop

            // dbg("partiters = "+partiters);
            // communicate results back to maze object
            maze.newMaze(root, cells, dists, startx, starty);
        }
        catch (InterruptedException ex) {
            // necessary to catch exception to avoid escalation
            // exception mechanism basically used to exit method in a controlled way
            // no need to clean up internal data structures
            // dbg("Catching signal to stop") ;
        }
    }

    static final int MAX_TRIES = 250 ;

    /**
     * Generate all rooms in a given maze where initially all walls are up. Rooms are placed randomly and of random sizes
     * such that the maze can turn out to be too small to accommodate the requested number of rooms (class attribute rooms). 
     * In that case less rooms are produced.
     * @return generated number of rooms
     */
    private int generateRooms() {
        // Rooms are randomly positioned such that it may be impossible to place the all rooms if the maze is too small
        // to prevent an infinite loop we limit the number of failed to MAX_TRIES == 250
        int tries = 0 ;
        int result = 0 ;
        while (tries < MAX_TRIES && result <= rooms) {
            if (placeRoom())
                result++ ;
            else
                tries++ ;
        }
        return result ;
    }

    /**
     * Notify the maze builder thread to stop the creation of a maze and to terminate
     */
    public void interrupt() {
        buildThread.interrupt() ;
    }



}
like image 489
user2309856 Avatar asked Mar 22 '23 07:03

user2309856


1 Answers

To unit test your protected methods, simply put your test class in the same package as the class you are looking to test (in this case falsted). Just because they are in the same package, it doesn't mean they have to be in the same directory (just a parallel test directory hierarchy).

For example, if you are using maven, your source would be in src/main/java/falsted and your tests would be in src/test/java/falsted. From a maven perspective, these are separate directories and therefore can easily be managed separately, while from a Java perspective, they are the same package (so protected methods are visible).

Test your constructors by probing the state of the object to ensure that all values got their default or initial value.

like image 198
Rob Avatar answered Mar 29 '23 03:03

Rob