Jest Testing, ...is not a function

Hello everyone!

I know FCC doesn’t cover testing with jest, but I’m hoping someone can help me out. I’ve already successfully exported a different factory function to a different jest test written in the same way, but this one doesn’t want to work. The problem function in question is the generateCoordinates method inside the gameboardFactory function. When I run the tests, I get the type error that gameboardFactory.generateCoordinates is not a function. I’m feeling like as ever it’s something simple I’m missing. Here are the two files:

gameboard.js


export let gameboardFactory = () => {

    let coordinates = {
        xsmall: null,
        small: null,
        medium: null
    }

    let checker = (arr, target) => {
        return target.every(value => arr.includes(value));
    }
    
    let generateCoordinates = (size, length) => {
      while (coordinates[size] == null) {
      let array = [];
      let flag = true;
      let subArray = [];
      for (let i = 0; i < 2; i++) {
           subArray.push(Math.ceil(Math.random() * (11 - length)))
          }
      array.push(subArray);
      for (let j = 0, value2 = (subArray[1] + 1); j < (length -1); j++, value2++) {
          let subsub = [subArray[0]]; 
        subsub.push(value2)
        array.push(subsub)
      }
      for (let k = 0; k < array.length; k++) {
        for (let l = 0; l < Object.values(coordinates).length; l++) {		if (Object.values(coordinates)[l] == null) continue;
        for (let m = 0; m < Object.values(coordinates)[l].length; m++) {
          if (checker(Object.values(coordinates)[l][m], array[k])) {
            flag = false;
          }
          }
       }
    }
        if (flag == true) {coordinates[size] = array;}
      }
    }

    
    


    return {
        coordinates,
        generateCoordinates
    }
}

gameboard.test.js

const gameboard = require('../functions/gameboard');
const gameboardFactory = gameboard.gameboardFactory;

describe('gameboard tests', () => {

    test('returns ship lengths', () => {
        const testGame = gameboardFactory.generateCoordinates('small', 3)

        expect(testGame.coordinates).toMatchObject({
            xsmall: null,
            small: [1,2],
            medium: null
        })
    })
})

Also, I am aware that the way I have the test set up is that even if the function was not causing an error, that the test would not pass. I’m just trying to see first what is actually returning as the result.

P.S. if anyone has any advice on refactoring this code I’m open to that as well, it’s definitely a bit messy!

Thanks so much!

This doesn’t have a method generateCoordinates (it’s a function so it has eg bind, call, apply &c):

This, however, does have a method generateCoordinates (it’s an object with two properties, that and coordinates):

const gameboardFactory = gameboard.gameboardFactory();

Edit:

Yes, I can suggest a few things. But can you explain in words what the purpose is here & how it works first, I don’t want to give you any advice that might turn out to not be applicable.

1 Like

@DanCouper Thanks for your help! So I’ve made the change you suggested and I’m now instead getting another TypeError saying that it can’t read property ‘coordinates’ of undefined (at expect(testGame.coordinates)).

As for the refactoring, it’s eventually going to be a battleship game, like the board game. What it’s doing here is the first draft of automatically creating coordinates for the ship placement. It takes the name of the ship ‘piece’ and length of the piece, and returns coordinates for each ship that don’t intersect with each other. As far as I can tell right now the code works from messing with in in JSFiddle, but I got stuck when I was trying to test it. What I don’t like is the abundance of for loops, it seems very opposite of best practices.

Ok. So issue here is this: if you run gameFactory(), it returns an object with those two properties: generateCoordinates, which is the function, and coordinates, which is this:

{
  xsmall: null,
  small: null,
  medium: null
}

It doesn’t matter how many times generateCoordinates runs, that object won’t change. You ran gameFactory, it gave you an object and a function back, that’s done.

The object assigned to the variable coordinates in the closure; that will change, but you’re not going to see an updated value there as it stands.

There are two perfectly valid solutions to this problem that will only require a very slight change. What you need to do is to get the latest value of that object assigned to the variable coordinates every time you ask for it

1 Like

@DanCouper Hmm okay, I’ve tried adding

coordinates = generateCoordinates();

before the return of gameboardFactory, and that has given me a new error

cannot read property 'small' of undefined

at

while (coordinates[size] == null)

which is pretty confusing, and also that solution doesn’t quite feel right. I’m also not sure if the change I should be making would be in the test file or the function’s file. Do you have any other hints? I really appreciate your help!

Change the coordinates property in the object returned from the factory to be either a getter or a normal function. The getter will mean you don’t have to change any other code that uses it, a normal function means you’ll need to call that every time you want the coordinates (that happens with a getter, it’s just implicit rather than explicit)

You need to get the coordinates object. That object changes every time you run generateCoordinates, so you need a function to pull the latest value of coordinates every time you want it.

1 Like

@DanCouper Okay, I’ve never used a getter before, but I think I’ve set one up. Here’s my updated code:

gameboard.js

export let gameboardFactory = () => {

    let coordinates = {
        xsmall: null,
        small: null,
        medium: null,
        get info() {
            return {xsmall: this.xsmall, 
                    small: this.small, 
                    medium: this.medium
                }
        }
    }

    let checker = (arr, target) => {
        return target.every(value => arr.includes(value));
    }
    
    let generateCoordinates = (size, length) => {
      while (coordinates[size] == null) {
      let array = [];
      let flag = true;
      let subArray = [];
      for (let i = 0; i < 2; i++) {
           subArray.push(Math.ceil(Math.random() * (11 - length)))
          }
      array.push(subArray);
      for (let j = 0, value2 = (subArray[1] + 1); j < (length -1); j++, value2++) {
          let subsub = [subArray[0]]; 
        subsub.push(value2)
        array.push(subsub)
      }
      for (let k = 0; k < array.length; k++) {
        for (let l = 0; l < Object.values(coordinates).length; l++) {		if (Object.values(coordinates)[l] == null) continue;
        for (let m = 0; m < Object.values(coordinates)[l].length; m++) {
          if (checker(Object.values(coordinates)[l][m], array[k])) {
            flag = false;
          }
          }
       }
    }
        if (flag == true) {coordinates[size] = array;}
      }
    }

    
    let getter = coordinates.info;


    return {
        coordinates,
        getter,
        generateCoordinates
    }
}

gameboard.test.js

const gameboard = require('../functions/gameboard');
const gameboardFactory = gameboard.gameboardFactory();

describe('gameboard tests', () => {

    test('provides coordinates', () => {
        const testGame = gameboardFactory.generateCoordinates('small', 3)
        const getter = gameboardFactory.getter;
        expect(getter).toMatchObject({
            xsmall: null,
            small: [1,2],
            medium: null
        })
    })
})

It’s definitely getting close; as it is now, the test is receiving the coordinates object, but the values are all still null. I thought that in the tests trying

test('provides coordinates', () => {
        const testGame = gameboardFactory.generateCoordinates('small', 3)
        const getter = gameboardFactory.getter;
        expect(testGame.getter).toMatchObject({
            xsmall: null,
            small: [1,2],
            medium: null
        })
    })

would work, but it doesn’t, saying that testGame is undefined.

Ok, so not like that. Doesn’t need that drastic a change. Just like this (getter version):

return {
  get coordinates() {
    return coordinates;
  },
  generateCoordinates,
}

Which is used like:

const myGameFactory = gameFactory();
myGameFactory.coordinates; // { small: null, ...etc }
myGameFactory.generateCoordinates("small", 3);
myGameFactory.coordinates; // { small: [1,2], ...etc }

Or like this (function version):

return {
  getCoordinates() {
    return coordinates;
  },
  generateCoordinates,
}

Which is used like:

const myGameFactory = gameFactory();
myGameFactory.getCoordinates(); // { small: null, ...etc }
myGameFactory.generateCoordinates("small", 3);
myGameFactory.getCoordinates(); // { small: [1,2], ...etc }

Note that I haven’t yet tested the actual logic used to generate anything in the generateCoordinates finction, so I can’t guarantee this will get over the first hurdle

2 Likes

Thank you @DanCouper, this has fixed the problem! I’ll definitely have to look further into using getters. I have one last question, the values I’m receiving in the jest test are consistent with what I expected, but in a format that I’m not used to seeing. Where I am expecting an array of arrays, ex:

[[1,  6],  [1,  7],  [1, 8]] 

I am instead receiving:

"small": Array [
         Array [
           1,
           6,
         ],
         Array [
           1,
           7,
         ],
         Array [
          1,
           8,
         ],
       ]

I assume it’s being formatted this way either because it’s being read from an object, or maybe because of jest, but is there a way to have it formatted “normally”? Or if not, can it still be accessed the same way? From the above example:

coordinates['small'][0][1] == 6      //true?

It’s printing it as a string, so it’s just printing what you would normally get in the console. As far as I can remember there is a way to print things according to a certain format, been a year or two since I touched Jest much though

I’ll have a look at the code for generation, I think there are a few things you can do to clean it up (and to ensure that you can easily test it as well, by controlling the randomness)

:slightly_smiling_face: No problem. Getters can be quite useful for stuff like this. They’re a bit magic though. They look like a normal property but they’re not always going to act like it here. Using a function (the second example, with getCoordinates()) is a bit more explicit.

1 Like

@DanCouper (or anyone who may be able to help!) Sorry to bother you again but I’ve run into another roadblock if you’ve got time.

This has to do with the function you helped me to correct, interacting with a new function written today. The new function (when functioning correctly) should take a coordinate in format [x, y], and compare it to the coordinates that were generated and stored in coordinates by the generateCoordinates function to see if any are a match. I believe the function as it is now functions correctly until once it has identified a match, and tries to run the hit() function on the corresponding object from the ships object. I believe the problem is the coordinatesIndex and valueIndex arguments, because if I hard code them respectively as 1 and 0, the code functions as designed. Here are the relevant files:

gameboard.js

const ship = require('./ship');
const shipFactory = ship.shipFactory;

export const ships = {
    xsmall: shipFactory(2),
    small: shipFactory(3),
    medium: shipFactory(4)
}

export let gameboardFactory = () => {

    let coordinates = {
        xsmall: null,
        small: null,
        medium: null,
        misses: []
    }

    let checker = (arr, target) => {
        return target.every(value => arr.includes(value));
    }
    
    let generateCoordinates = (size) => {
      while (coordinates[size] == null) {
      let array = [];
      let flag = true;
      let subArray = [];
      for (let i = 0; i < 2; i++) {
           subArray.push(Math.ceil(Math.random() * (11 - ships[size].length)))
          }
      array.push(subArray);
      for (let j = 0, value2 = (subArray[1] + 1); j < (ships[size].length -1); j++, value2++) {
          let subsub = [subArray[0]]; 
        subsub.push(value2)
        array.push(subsub)
      }
      for (let k = 0; k < array.length; k++) {
        for (let l = 0; l < Object.values(coordinates).length; l++) {		if (Object.values(coordinates)[l] == null) continue;
        for (let m = 0; m < Object.values(coordinates)[l].length; m++) {
          if (checker(Object.values(coordinates)[l][m], array[k])) {
            flag = false;
          }
          }
       }
    }
        if (flag == true) {coordinates[size] = array;}
      }
    }

    let receiveAttack = (attackLocation) => {
      Object.values(coordinates).forEach((val, coordinatesIndex) => {
        val.forEach((pair, valueIndex) => {
          if (checker(pair, attackLocation)) {
            let damagedShip = Object.keys(ships)[coordinatesIndex]
            ships[damagedShip].hit(valueIndex);
          }
          else {
            coordinates.misses.push(attackLocation) 
          }
        })
      })
    }


    return {
        get coordinates() {
            return coordinates
        },
        generateCoordinates,
        receiveAttack
    }
}

gameboard.test.js (using test 3)

const gameboard = require('../functions/gameboard');
const gameboardFactory = gameboard.gameboardFactory();
const ships = gameboard.ships

describe('gameboard tests', () => {

    test('provides coordinates', () => {
        const newGame = gameboardFactory;
        Object.keys(gameboard.ships).forEach(key => {
            newGame.generateCoordinates(`${key}`)
        });
        expect(newGame.coordinates).not.toMatchObject({
            xsmall: null,
            small: null,
            medium: null,
            misses: []
        })
    })

    test('coordinates are accessible', () => {
        const newGame = gameboardFactory;
        Object.keys(gameboard.ships).forEach(key => {
            newGame.generateCoordinates(`${key}`)
        });
        expect(typeof newGame.coordinates.small[0][0]).toBe('number');
    })

    test('receiveAttack functions correctly', () => {
        const newGame = gameboardFactory;
        Object.keys(gameboard.ships).forEach(key => {
            newGame.generateCoordinates(`${key}`)
        });
        newGame.coordinates.small = [[1, 1], [1, 2], [1, 3]];
        newGame.receiveAttack([1, 1]);
        expect(ships.small.hits).toBe([false, false, false]);
    })
})

Please if you have time take a look!

As always thank you so much!

1 Like

For anyone looking I have made a couple small changes but am experiencing the same problem. Here is my updated file:

const ship = require('./ship');
const shipFactory = ship.shipFactory;

export const ships = {
    xsmall: shipFactory(2),
    small: shipFactory(3),
    medium: shipFactory(4)
}

export let gameboardFactory = () => {

    let coordinates = {
        xsmall: null,
        small: null,
        medium: null,
        misses: []
    }

    let checker = (arr, target) => {
      return	arr.length === target.length &&
        arr.every((c, i) => c === target[i]);
          }
    
    let generateCoordinates = (size) => {
      while (coordinates[size] == null) {
      let array = [];
      let flag = true;
      let subArray = [];
      for (let i = 0; i < 2; i++) {
           subArray.push(Math.ceil(Math.random() * (11 - ships[size].length)))
          }
      array.push(subArray);
      for (let j = 0, value2 = (subArray[1] + 1); j < (ships[size].length -1); j++, value2++) {
          let subsub = [subArray[0]]; 
        subsub.push(value2)
        array.push(subsub)
      }
      for (let k = 0; k < array.length; k++) {
        for (let l = 0; l < Object.values(coordinates).length; l++) {		if (Object.values(coordinates)[l] == null) continue;
        for (let m = 0; m < Object.values(coordinates)[l].length; m++) {
          if (checker(Object.values(coordinates)[l][m], array[k])) {
            flag = false;
          }
          }
       }
    }
        if (flag == true) {coordinates[size] = array;}
      }
    }

    let receiveAttack = (attackLocation) => {
      Object.values(coordinates).forEach((val, coordinatesIndex) => {
        if (val == null) return;
        val.forEach((pair, valueIndex) => {
          if (checker(pair, attackLocation)) {
            let damagedShip = Object.keys(ships)[coordinatesIndex]
            ships[damagedShip].hit(valueIndex);
          }
          else {
            coordinates.misses.push(attackLocation) 
          }
        })
      })
    }


    return {
        get coordinates() {
            return coordinates
        },
        generateCoordinates,
        receiveAttack
    }
}

Thank You!

update: I’ve somehow solved the issue, just by retyping the problem function in JSFiddle, I guess there was a mistype in the original somewhere. I’ll try to find where it was later, posting the solution code for now.

let receiveAttack = (attackLocation) => {
      let flag = false
      Object.values(coordinates).forEach((val, coordinatesIndex) => {
        if (val == null) return
        val.forEach((pair, valueIndex) => {
          if (checker(pair, attackLocation)) {
            let damage = Object.keys(ships)[coordinatesIndex];
            ships[damage].hit(valueIndex)
            flag = true;
          }
        })
        })
        if (flag == false) {
          coordinates.misses.push(attackLocation);
        }
    }