(This post is part of my “from scratch” AngularJS project. If you are feeling lost, the first post is here.)
Previously, I had mentioned that test driven development hasn’t really “stuck” for me and I find myself switching back to BDD for the most part. I find that this is usually because I don’t plan features out far enough in advance and I’m more of the “experiment and refactor” kind of developer. I have found instances where TDD actually works better for me, and today’s topic is one of those instances.
We are going to be working on the card service. This service will allow us to get a deck of cards, shuffle the deck, deal from the deck. Since this blackjack project is a game based off of set rules, we know how the cards should behave. We can write tests against these rules and then we’ll write our service to meet the rules. The rules are:
- We should be able to get a new deck of cards with the object type “Deck”
- The deck should contain 52 cards
- There should be no duplicate cards in a deck
- Each card should have a rank and a suit.
- We should be able to ‘deal’ a card from a deck that has undealt cards in it.
- Attempting to ‘deal’ from a deck with no undealt cards returns false
- When a card is dealt, it is no longer in the cards array
- We should be able to shuffle the deck and randomize all undealt cards.
- We should be able to ‘reset’ a deck that will move all dealt cards into the undealt status and shuffle.
Now let’s write those tests:
describe('CardService Unit Tests', function () { var CardService; beforeEach(function () { module('blackjack.card'); inject(function (_CardService_) { CardService = _CardService_; }); }); it('should be true', function () { expect(true).toBe(true); }); describe('Deck related tests', function () { var deck; beforeEach(function () { deck = CardService.newDeck(); }); it('should be able to get a new deck of cards with the object type "Deck"', function () { expect(deck instanceof CardService.Deck).toBe(true); }); it('The deck should contain 52 cards', function () { expect(deck.cards.length).toBe(52); }); it('Each card should have a value and a rank.', function () { deck.cards.forEach(function (card) { expect(card.rank).toBeDefined(); expect(card.suit).toBeDefined(); }); }); it('There should be no duplicate cards in a deck', function () { var cardNames = []; deck.cards.forEach(function (card) { expect(cardNames.indexOf(card.name())).toBe(-1); cardNames.push(card.name()); }); }); it('We should be able to "deal" a card from a deck that has undealt cards in it.', function () { expect(deck.deal).toBeDefined(); var card = deck.deal(); expect(card instanceof CardService.Card).toBe(true); }); it('Attempting to "deal" from a deck with no undealt cards returns false', function () { deck.cards = []; var card = deck.deal(); expect(card).toBe(false); }); it('When a card is dealt, it is no longer in the cards array', function () { var card = deck.deal(); expect(deck.cards.indexOf(card)).toBe(-1); }); it('We should be able to shuffle the deck and randomize all undealt cards.', function () { expect(deck.shuffle).toBeDefined(); var originalDeck = angular.copy(deck.cards); deck.shuffle(); expect(originalDeck).not.toEqual(deck.cards); }); it('We should be able to "reset" a deck that will move all dealt cards into the undealt status and shuffle.', function () { expect(deck.reset).toBeDefined(); deck.deal(); expect(deck.cards.length).not.toBe(52); deck.reset(); expect(deck.cards.length).toBe(52); }); }); });
So we’ve got the “T” part of our TDD done. Let’s start working on getting these tests passing. The Karma runner will watch our files for changes and rerun the tests. In my opinion, the fun part about doing TDD is watching the tests start passing. This is the implementation to pass those tests:
https://github.com/adamweeks/angular-blackjack/blob/blog-post-6/src/app/card/card.service.js
Once all of the tests are passing, we should be fairly confident that our service is ready to be used. Let’s put it in place and start dealing cards! We’re only making a few minor changes to game.controller.js:
game.init = function () { game.started = false; game.player = PlayerService.newPlayer('Ringo', 100); game.deck = CardService.newDeck(); }; game.start = function () { game.started = true; game.player.changeScore(-100); game.deck.shuffle(); game.playerCards = []; game.hit(); game.hit(); }; game.hit = function () { game.playerCards.push(game.deck.deal()); };
Our controller now has a deck object, as well as a playerCards array. Calling the hit function tells the deck to deal a card into the playerCards array. On the view side, we’ll wire up a button that will fire the hit function, as well as displaying the cards via an ng-repeat.
You’ll notice that we don’t have any actual blackjack game logic in place yet, and a player can keep dealing themselves cards until the deck runs out and they still win! All of that is for yet another blog post! We want to keep things as single-responsibility as possible. Consider an actual deck of cards and a dealer. If you were to place them down on any table, they could deal and shuffle cards regardless of what game they were playing. Yes, they would be a pretty terrible dealer if they didn’t have anyone enforcing the rules on them, but the cards wouldn’t change and the act of dealing wouldn’t change. That is what we are trying to recreate with this service. Now, if we wanted to build a poker app, we could use this exact service to deal cards without having to change a line of code!
View the results of our code here.
Up Next: Gulp!