DEV Community

Anur
Anur

Posted on

Steam Backlog Helper: Tests and Minor improvements

I haven't been extremely consistent with this last blog post - I apologize, work has been intense! In this post I will be going through a couple Unit Tests (SOME because there is a LOT of them, far too many to go through them all), and discussing some minor features I've implemented since the last time.

Unit Tests

Games Service

The Games Service is responsible for a lot of the core features of Steam Backlog Helper:

  • Store user's games
  • Filter and accordingly assign games to the metadata worker
  • Load games from the database (no metadata fetching)
  • Get user's games with metadata
  • Get user's games' genres
  • Get recommended games (could and should probably move this to another module)
  • Search games

First and foremost, I hardcoded some mock values that I will be using to test the service

const testSteamId = '123456789';
const mockGames = [
        {
          appid: 1,
          name: 'Game A',
          genres: ['Action', 'RPG'],
          iscompleted: false,
          expanded: false,
          rating: 4.5
        },
        {
          appid: 2,
          name: 'Game B',
          genres: ['Action'],
          iscompleted: false,
          expanded: false,
          rating: 3
        },
        {
          appid: 3,
          name: 'Game C',
          genres: ['Puzzle'],
          iscompleted: true,
          expanded: false,
          rating: 5
        }
];
Enter fullscreen mode Exit fullscreen mode

Then, I mocked and imported all the necessary providers:

describe('GamesServiceService', () => {
  let service: GamesServiceService;
  let ownedRepo: jest.Mocked<Repository<OwnedGame>>;
  let usersRepo: jest.Mocked<Repository<User>>;
  let gameMetadata: jest.Mocked<Repository<GameMetadata>>;
  let metadataQueue: MetadataQueue;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        GamesServiceService,
        {
          provide: getRepositoryToken(OwnedGame),
          useValue: {
            save: jest.fn(),
            create: jest.fn(),
            insert: jest.fn(),
            find: jest.fn(),
            findOne: jest.fn(),
            update: jest.fn(),
            upsert: jest.fn(),
            createQueryBuilder: jest.fn()
          }
        },
        {
          provide: getRepositoryToken(User),
          useValue: {
            save: jest.fn(),
            create: jest.fn(),
            insert: jest.fn(),
            find: jest.fn(),
            findOne: jest.fn(),
            update: jest.fn()
          }
        },
        {
          provide: getRepositoryToken(GameMetadata),
          useValue: {
            save: jest.fn(),
            create: jest.fn(),
            insert: jest.fn(),
            find: jest.fn(),
            findOne: jest.fn(),
            update: jest.fn(),
            findBy: jest.fn()
          }
        },
        {
          provide: HttpService,
          useValue: {
            get: jest.fn(),
            post: jest.fn()
          }
        },
        {
          provide: MetadataQueue,
          useValue: {
            addFetchJob: jest.fn()
          }
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn()
          }
        }
      ],
    }).compile();

    service = module.get<GamesServiceService>(GamesServiceService);
    ownedRepo = module.get(getRepositoryToken(OwnedGame));
    gameMetadata = module.get(getRepositoryToken(GameMetadata));
    usersRepo = module.get(getRepositoryToken(User));
    metadataQueue = module.get(MetadataQueue);
  });
Enter fullscreen mode Exit fullscreen mode

After the setup is complete, we can move on to the actual tests.

Should throw error when user has no games

it('should throw error when user has no games', async () => {
      jest.spyOn(usersRepo, 'findOne').mockResolvedValue(null);
      jest.spyOn(usersRepo, 'create').mockReturnValue({ id: 123, steam_id: testSteamId } as User);
      jest.spyOn(usersRepo, 'save').mockResolvedValue({ id: 123, steam_id: testSteamId } as User);

      jest.spyOn(service['http'], 'get').mockReturnValue({
        toPromise: jest.fn(),
      } as any);

      jest.spyOn(require('rxjs'), 'firstValueFrom').mockResolvedValue({
        data: {
          response: {
            games: []
          }
        }
      });

      await expect(service.fetchAndStoreUserGames(testSteamId)).rejects.toThrow('User has no existing metadata');
  });
Enter fullscreen mode Exit fullscreen mode

This one is pretty simple. First we mockResolvedValue for the methods the service is going to call. Simply put, we are telling the repo and service what they are going to return once they've been called. We return null when the service searches for a user to check if it actually enters this condition and then force it to create a new one - one that we mocked earlier.

Should create user if missing on fetch & store user games

This one's a bit larger, but not too complicated either. The beginning is similar to the previous test, we check if it creates a user if one does not already exist.

jest.spyOn(usersRepo, 'findOne').mockResolvedValue(null);
    jest.spyOn(usersRepo, 'create').mockReturnValue({ id: 123, steam_id: testSteamId } as User);
    jest.spyOn(usersRepo, 'save').mockResolvedValue({ id: 123, steam_id: testSteamId } as User);
    jest.spyOn(service['http'], 'get').mockReturnValue({
      toPromise: jest.fn(),
    } as any);

Enter fullscreen mode Exit fullscreen mode

Next step: mock the Steam API response:

const fakeGame = {
      appid: 1,
      playtime_forever: 100,
      rtime_last_played: 1234567890,
    };

    jest.spyOn(require('rxjs'), 'firstValueFrom').mockResolvedValue({
      data: {
        response: {
          games: [fakeGame]
        }
      }
    });
Enter fullscreen mode Exit fullscreen mode

We can do this because we are spying on the http.get() request, which is present in the service, as seen below:

const response = await firstValueFrom(
      this.http.get<GameResponse>(
        'https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/',
        {
          params: {
            key: this.config.get('STEAM_KEY'),
            steamid: steamId,
            include_appinfo: 1,
            format: 'json',
          },
        },
      ),
    );
Enter fullscreen mode Exit fullscreen mode

After mocking a new user, and mocking a response from Steam API, we have to check the next step: adding a game to user's owned games as well as adding a job to the worker. Couple of lines of code do this for us:

jest.spyOn(ownedRepo, 'upsert').mockResolvedValue({} as any);
    jest.spyOn(gameMetadata, 'findBy').mockResolvedValue([]);
    jest.spyOn(metadataQueue, 'addFetchJob').mockResolvedValue(undefined);
Enter fullscreen mode Exit fullscreen mode

Last but not least, let's call the function and check if everything lines up:

const result = await service.fetchAndStoreUserGames(testSteamId);
    expect(usersRepo.create).toHaveBeenCalledWith({ steam_id: testSteamId});
    expect(usersRepo.save).toHaveBeenCalledTimes(1);
    expect(ownedRepo.upsert).toHaveBeenCalled();
    expect(metadataQueue.addFetchJob).toHaveBeenCalledTimes(1);
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      appid: fakeGame.appid,
      playtime_minutes: fakeGame.playtime_forever,
      loadingMetadata: true,
      expanded: false,
    });
Enter fullscreen mode Exit fullscreen mode

As you can see, we expect that a new user should be created with our fake steam id, saved only once | that one job should be added to the metadata queue, and result (user's games with metadata) to hold an object which matches our mocked game (+ some extra fields that it gets assigned in the service).

There are 4 more tests in this suite, and a total of 16(!) suites in total, so writing about all of them here would take ages. However, I suppose you get the gist from these few.

All controllers and services are tested in SBH.

Minor changes & new features

Editing profile
Currently, we only support editing the description, however in the near future, we will support changing the gif next to user information.

Game Status Overriding
Implement functionality to override the automatically determined completion status, in order to improve recommendation results and reduce clutter

Game Details Page
Finished the Game Details Page, where users can see pictures and videos from the selected games and some additional data (such as their progress)!

I think this nicely sums up the latest changes. I'll write soon, when I have even more features and hopefully soon once SBH is live!

Top comments (0)