mardi 7 février 2023

Testing mediator design pattern with Jest

I'm currently working on a web based Battleship game for one my Odin Project assignments. At some point I felt that the mediator pattern would be the perfect choice to deal with firing missiles for the player and the CPU. Now, my assignment encourages me to test the game thoroughly without console.log but with Jest. I've been able to test some features of the game but the mediator pattern is confusing. Mocking functions or modules is likely to be the right direction to go for but to be honest I've read heaps of guides and I've not been able to implement them (understanding mocks has been hard as well). The function notifyAttackinside EventManager has already been tested the old way with console.log.

Can anyone let me know what I'm doing wrong please?

Event Manager

export {EventManager}

const EventManager = {
  gameManager: GameManager,
  notifyAttack(who, coordinate){
    if(!who)
      throw new Error(`Unknown player`);
    else
      who === `CPU` ? GameManager.player.board.getAttack(coordinate) : GameManager.cpu.board.getAttack(coordinate);

    GameManager.turn = who;
  }
}

Game Manager

import {Player} from "./player";
export {GameManager}

const GameManager = {
  turn: undefined,
  player: undefined,
  cpu: Player(),
}

Player

import {coordinate, GameBoard} from './gameboard';
import { EventManager } from './eventmanager';
export {Player}

const playerActions = {
  eventManager: EventManager,
  fire(coordinate){
    this.eventManager.notifyAttack(this.name, coordinate);
  }
}

function Player(name){
  const player = Object.create(playerActions);
  player.board = GameBoard();
  name === undefined ? player.name = `CPU`: player.name = name;
  return player;
}

GameBoard

import {  Ship } from "./ship"
export {GameBoard, coordinate, shipOrientation, tile}

function coordinate(x,y){
  const boardSize = 10;
  if(x > boardSize || x < 1)
    throw new Error(`X coordinate is out of boundaries`);
  if(y > boardSize || y < 1)
    throw new Error(`Y coordinate is out of boundaries`);
  return{x:x, y:y}
}

function tile(coordinate, id){
  return{coordinate: coordinate, id: id}
}

const shipOrientation = {
  HORIZONTAL: Symbol(`horizontal`),
  VERTICAL: Symbol(`vertical`),
}

const gameboardActions = {
  placeShips(shipType, orientation, inputCoordinate){
    const ship = Ship(shipType);
    ship.ID = `${inputCoordinate.x},${inputCoordinate.y}`;
  
    this.tiles.forEach(tile=>{
      if(tile.coordinate.x === inputCoordinate.x && tile.coordinate.y === inputCoordinate.y)
      throw new Error(`There's already an object on that input coordinate`);
    })

    if(orientation === shipOrientation.HORIZONTAL){
      if(inputCoordinate.x + ship.length > this.size)
        throw new Error(`Part of ship is out of board X boundary`);
      for(let i = 0; i<ship.length; ++i)
        this.tiles.push(tile(coordinate(inputCoordinate.x+i, inputCoordinate.y), `${ship.ID}`));
    }else if(orientation === shipOrientation.VERTICAL){
      if(inputCoordinate.y + ship.length > this.size)
        throw new Error(`Part of ship is out of board Y boundary`);
      for(let i = 0; i<ship.length; ++i)
        this.tiles.push(tile(coordinate(inputCoordinate.x, inputCoordinate.y+i), `${ship.ID}`));
    }else
      throw new Error(`Undefined ship orientation`);

    this.shipsLog.set(`${ship.ID}`,ship);
  },

  getAttack(inputCoordinate){
    let isShip, ID;
    this.tiles.forEach(tile=>{
      if(tile.coordinate.y===inputCoordinate.y&&tile.coordinate.x===inputCoordinate.x&&tile.id){
        ID = tile.id;
        return isShip = true;
      }
    })

    if(isShip){
      this.shipsLog.get(ID).hit()
      if(this.shipsLog.get(ID).isSunk){
        this.removeShip(ID);
        this.checkSunkFleet();
      }
    }else
      this.tiles.push(tile(inputCoordinate, undefined));
  },

  removeShip(ID){
    this.shipsLog.delete(ID);
    for(let i = 0; i<this.tiles.length; ++i)
      if(this.tiles[i].id===ID)
        this.tiles.splice(i,1);
  },

  checkSunkFleet(){
    this.shipsLog.size === 0 ? this.sunkFleet=true:this.sunkFleet=false;
  }

}


function GameBoard (){
  const gameboard = Object.create(gameboardActions);
  gameboard.shipsLog = new Map();
  gameboard.tiles= []; 
  gameboard.size= 10;
  gameboard.sunkFleet = false;

    return gameboard;
}

Jest test

import {GameBoard, coordinate} from "./gameboard";
import {GameManager} from './gamemanager';
import {Player} from "./player";
import {EventManager} from "./eventmanager";

GameManager.player = Player(`Pablo`);

describe(`Player set up`, ()=>{
  test(`Player's name is Pablo`,()=>{
    expect(GameManager.player.name).toMatch(/^[A-Z]+$/i); 
  });
  test(`Player has a board to play with`, ()=>{
    expect(GameManager.player.board).toMatchObject(GameBoard());
  });
})

describe(`Player's actions`,()=>{
  test(`Pablo fires a missile, he misses ship target though`, ()=>{
    const myCoordinate = coordinate(5,5);
    const spy = jest.spyOn(EventManager, 'notifyAttack')
    GameManager.player.fire(myCoordinate);
    expect(spy).toBeCalled();
    expect(GameManager.cpu.getAttack).toBeCalledWith(myCoordinate);
    expect(GameManager.cpu.shipsLog.has(`${myCoordinate.x}, ${myCoordinate.y}`));
  })
})

So the flow goes in this way:

  1. A Player already set up in GameManager (Pablo) fires a missile by triggering fire() inside the Player object
  2. fire() reports EventManager who fires the missile and its coordinates
  3. EventManager calls CPU GameBoard getAttack() method that records Pablo's missing missile

You guys might wonder why I'm using an EventManager instead of relying on GameManager. Basically, GameManager is in charge of changing turns, set up the game whereas EventManager specifically deals with the battle to prevent coupling between Player and CPU

I'd like to hear from you if you need more details for the question.

Cheers!

Aucun commentaire:

Enregistrer un commentaire