SlideShare a Scribd company logo
Client-Side Unit Testing



                       Cloud Chen
                       2012/5/11
You don’t write tests
You know you should,
    but you don’t
You won’t be blamed for it
Because...
Client Side Unit Testing
There are many common issues
   that prevent developers
       for writing tests
You think
You think
•   Tests are not necessary and irrelevant
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
•   Lacking of specification
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
•   Lacking of specification
•   Spaghetti code are hard for testing
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
•   Lacking of specification
•   Spaghetti code are hard for testing
•   Web application needs functional testing rather
    than unit testing
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
•   Lacking of specification
•   Spaghetti code are hard for testing
•   Web application needs functional testing rather
    than unit testing
•   Unit testing looks like only useful for back-end
    code rather than front-end
You think
•   Tests are not necessary and irrelevant
•   Manual testing is enough
•   Lacking of specification
•   Spaghetti code are hard for testing
•   Web application needs functional testing rather
    than unit testing
•   Unit testing looks like only useful for back-end
    code rather than front-end
•   Lazy...
Our goal
Our goal

  Help you start
 writing unit tests
for front-end code
However,
Let’s start with another
     type of testing
Functional Testing
What is functional testing

 Functional testing is a type of black box
 testing that bases its test cases on the
 specifications of the software component
 under test. Functions are tested by feeding
 them input and examining the output, and
 internal program structure is rarely
 considered.
What is functional testing

 Functional testing is a type of black box
 testing that bases its test cases on the
 specifications of the software component
 under test. Functions are tested by feeding
 them input and examining the output, and
 internal program structure is rarely
 considered.
In a nutshell
In a nutshell

• Written from user perspective
In a nutshell

• Written from user perspective
• Proving users are able to reproduce defined steps
In a nutshell

• Written from user perspective
• Proving users are able to reproduce defined steps
• Don’t need to consider internal program structure
In a nutshell

• Written from user perspective
• Proving users are able to reproduce defined steps
• Don’t need to consider internal program structure
• Automating manual testing
Unit Testing
What is unit testing

unit testing is a method by which individual
units of source code, sets of one or more
computer program modules together with
associated control data, usage procedures, and
operating procedures, are tested to determine
if they are fit for use.
What is unit testing

unit testing is a method by which individual
units of source code, sets of one or more
computer program modules together with
associated control data, usage procedures, and
operating procedures, are tested to determine
if they are fit for use.
In a nutshell
In a nutshell
• Unit testing is complete isolation
In a nutshell
• Unit testing is complete isolation
• Must irrelevant to external dependencies
In a nutshell
• Unit testing is complete isolation
• Must irrelevant to external dependencies
• All unreliable or slow dependencies of a
  tested unit should be stubbed
In a nutshell
• Unit testing is complete isolation
• Must irrelevant to external dependencies
• All unreliable or slow dependencies of a
  tested unit should be stubbed
• Only the logic of that single unit is exercised
In a nutshell
• Unit testing is complete isolation
• Must irrelevant to external dependencies
• All unreliable or slow dependencies of a
  tested unit should be stubbed
• Only the logic of that single unit is exercised
• Must be fast
What are differences between
 functional and unit testing
What are differences between
 functional and unit testing
                    unit            functional


perspective     programmer              user

                every module        every process
   goal
              works as expected   works as expected

                  all cases
 coverage                          most user cases
               even edge cases

  result       programmer :)           user :)
What are differences between
 functional and unit testing
                    unit            functional


perspective     programmer              user

                every module        every process
   goal
              works as expected   works as expected

                  all cases
 coverage                          most user cases
               even edge cases

  result       programmer :)           user :)
What are differences between
 functional and unit testing
                    unit            functional


perspective     programmer              user

                every module        every process
   goal
              works as expected   works as expected

                  all cases
 coverage                          most user cases
               even edge cases

  result       programmer :)           user :)
What are differences between
 functional and unit testing
                    unit            functional


perspective     programmer              user

                every module        every process
   goal
              works as expected   works as expected

                  all cases
 coverage                          most user cases
               even edge cases

  result       programmer :)           user :)
What are differences between
 functional and unit testing
                    unit            functional


perspective     programmer              user

                every module        every process
   goal
              works as expected   works as expected

                  all cases
 coverage                          most user cases
               even edge cases

  result       programmer :)           user :)
Hence..
Hence..
•   We are programmer
Hence..
•   We are programmer
•   We care our code quality
Hence..
•   We are programmer
•   We care our code quality
•   We should write unit test code
Why, When
     of
unit testing
Why need unit testing
Why need unit testing

• Ensuring every component is bug-free
Why need unit testing

• Ensuring every component is bug-free
• Ensuring every component is easy to modify
Why need unit testing

• Ensuring every component is bug-free
• Ensuring every component is easy to modify
• Easy to locating bug of complex logic
Why need unit testing

• Ensuring every component is bug-free
• Ensuring every component is easy to modify
• Easy to locating bug of complex logic
• Preventing and capturing regression bug
Why need unit testing

• Ensuring every component is bug-free
• Ensuring every component is easy to modify
• Easy to locating bug of complex logic
• Preventing and capturing regression bug
• Testable code must be high readability and
  maintainability code
Why need unit testing

• Ensuring every component is bug-free
• Ensuring every component is easy to modify
• Easy to locating bug of complex logic
• Preventing and capturing regression bug
• Testable code must be high readability and
  maintainability code
• and so on...
When to do unit testing
When to do unit testing



• When you write your own classes, modules,
  libraries, frameworks.
When to do unit testing



• When you write your own classes, modules,
  libraries, frameworks.
• You don’t need to write test code for
  fundamental framework
testing style comparison
testing style comparison
                    TDD                    BDD


 based on      function oriented     feature oriented


   syntax      testing language          idiomatic


spring from      programmer          stakeholder || PO


accumulation     test as code      test as documentation
testing style comparison
                    TDD                    BDD


 based on      function oriented     feature oriented


   syntax      testing language          idiomatic


spring from      programmer          stakeholder || PO


accumulation     test as code      test as documentation
testing style comparison
                    TDD                    BDD


 based on      function oriented     feature oriented


   syntax      testing language          idiomatic


spring from      programmer          stakeholder || PO


accumulation     test as code      test as documentation
testing style comparison
                    TDD                    BDD


 based on      function oriented     feature oriented


   syntax      testing language          idiomatic


spring from      programmer          stakeholder || PO


accumulation     test as code      test as documentation
testing style comparison
                    TDD                    BDD


 based on      function oriented     feature oriented


   syntax      testing language          idiomatic


spring from      programmer          stakeholder || PO


accumulation     test as code      test as documentation
testing style comparison
testing style comparison


• It doesn’t matter which style you choose
testing style comparison


• It doesn’t matter which style you choose
• It does matter how many cases you have
testing style comparison
testing style comparison

 As we are using Scrum,
testing style comparison

 As we are using Scrum,
 BDD is more suitable for us.
BDD Framework
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
•   Be able to extend matcher
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
•   Be able to extend matcher
•   Be able to run in several environments
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
•   Be able to extend matcher
•   Be able to run in several environments
    1. Browser
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
•   Be able to extend matcher
•   Be able to run in several environments
    1. Browser
    2. CI
BDD Framework
•   Focuses on assertion, doesn’t depend on DOM
•   Succinct API
•   Natively support Spy
•   Support spec helper
•   Be able to extend matcher
•   Be able to run in several environments
    1. Browser
    2. CI
    3. NodeJS
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');                 Suite
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });
    afterEach(function () {                                    Inner Suite
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,

              });
                    "IsWordIKnow": false
                                                                       Spec
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);        Expectation
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });                                  Matcher
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');         Setup
    });
    afterEach(function () {
    });

    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Typical Spec & Succinct API
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');         Setup
    });
    afterEach(function () {
    });                                                      Tear down
    describe(“when initialized”, function() {
        beforeEach(function() {
            this.flashcard = new this.Flashcard({
                "Value": "here",
                "Translation": "这儿",
                    "Audio": "here_en.mp3",
                    "IsWord": true,
                    "ContentId": 174087,
                    "IsWordIKnow": false
              });
        });

        it(“composite key should be composed by ContentId and IsWord properties”,
function() {
             var expected_url = this.flashcard.get('ContentId') + '-' +
this.flashcard.get('IsWord');
             expect(this.flashcard.id).toEqual(expected_url);
        });
    });
Other Native Matchers
expect(x).toEqual(y);
expect(x).toBe(y);
expect(x).toMatch(pattern);
expect(x).toBeDefined();
expect(x).toBeUndefined();
expect(x).toBeNull();
expect(x).toBeTruthy();
expect(x).toBeFalsy();
expect(x).toContain(y);
expect(x).toBeLessThan(y);
expect(x).toBeGreaterThan(y);
expect(function(){fn();}).toThrow(e);
Other Native Matchers
expect(x).not.toEqual(y);
expect(x).not.toBe(y);
expect(x).not.toMatch(pattern);
expect(x).not.toBeDefined();
expect(x).not.toBeUndefined();
expect(x).not.toBeNull();
expect(x).not.toBeTruthy();
expect(x).not.toBeFalsy();
expect(x).not.toContain(y);
expect(x).not.toBeLessThan(y);
expect(x).not.toBeGreaterThan(y);
expect(function(){fn();}).not.toThrow(e);
Spec Helper
beforeEach(function() {
  this.fixtures = {
      Flashcard: {
        valid: { // response starts here
          "Value": "here",
          "Translation": "这儿",
           "Audio": "here_en.mp3",
           "IsWord": true,
           "ContentId": 174087,
           "IsWordIKnow": false
         },
         error: { // response starts here
             "ErrorCode": "101",
             "IsSuccess": "false"
         }
     }
 }
Spec Helper
beforeEach(function() {
  this.fixtures = {
      Flashcard: {
        valid: { // response starts here
          "Value": "here",
          "Translation": "这儿",
            "Audio": "here_en.mp3",
            "IsWord": true,
            "ContentId": 174087,
            "IsWordIKnow": false
          },
          error: { // response starts here
              "ErrorCode": "101",
              "IsSuccess": "false"
          }
      }
 }

     Using this.fixtures.Flashcard.valid to access pre-defined fixture
     for testing when this spec file is included in your spec runner.
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });

            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });
});
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);                       spy on instance method
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });

            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });
});
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });                               original function won’t be called
            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });
});
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });

            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });                                   match spied function was called
});
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });

            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });
});                         arguments of last call
Spy
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when initialized”, function() {
          beforeEach(function() {
              spyOn(jQuery, ‘ajax’);
              this.flashcard = new this.Flashcard();
              this.flashcard.fetch();
          });

            it(“should communicate with back-end via http get method”, function() {
                expect(jQuery.ajax).toHaveBeenCalled();
                expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’);
            });
      });
});                         arguments of last call      type of first argument
Extending Matcher
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.
Extending Matcher
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.




   jasmine-jquery comes to rescue
Matcher from
            jasmine jquery
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.
Matcher from
            jasmine jquery
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.

expect($(‘a’).toHaveClass(‘s-selected’);
Matcher from
            jasmine jquery
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.

expect($(‘a’).toHaveClass(‘s-selected’);
   expectation failed:
Matcher from
            jasmine jquery
<a href=”#” class=”link”>this is a link</a>
expect($(‘a’).hasClass('s-selected')).toBeTruthy();
   expectation failed:
       Expected false to be truthy.

expect($(‘a’).toHaveClass(‘s-selected’);
   expectation failed:
       Expected '<a></a>' to have class 's-selected'.
jasmine-jquery features

• a set of custom matchers for jQuery
  framework
• an API for handling HTML fixtures in your
  specs
jasmine-jquery matchers
expect(x).toBeHidden()
expect(x).toBeVisible()
expect(x).toHaveAttr(attributeName, attributeValue)
expect(x).toHaveProp(propertyName, propertyValue)
expect(x).toHaveText(string)
expect(x).toHaveHtml(string)
expect(x).toHaveId(id)
expect(x).toBeDisabled()
expect(x).toBeFocused()
expect(x).toHandle(eventName)
jasmine-jquery fixtures
In myfixture.html file:

<div id="my-fixture">some complex content here</div>

Inside your test:

loadFixtures('myfixture.html');
$('#my-fixture').myTestedPlugin();
expect($('#my-fixture')).to...;
Running Jasmine
Standalone Runner




    * Manually manage of your project files and specs
Running Jasmine
Dedicated Server runs jasmine
Ruby jasmine Gem




     * Using yaml manages of your project files and specs
Recap rules of Unit Testing

• Unit testing is complete isolation
• Must irrelevant to external dependencies
• All unreliable or slow dependencies of a
  tested unit should be stubbed
• Only the logic of that single unit is exercised
• Must be fast
Recap rules of Unit Testing

• Unit testing is complete isolation
• Must irrelevant to external dependencies
• All unreliable or slow dependencies of a
  tested unit should be stubbed
• Only the logic of that single unit is exercised
• Must be fast
Stub every dependency
Stub every dependency

• Native spy feature of Jasmine is not enough
Stub every dependency

• Native spy feature of Jasmine is not enough
• It doesn’t support fake timer
Stub every dependency

• Native spy feature of Jasmine is not enough
• It doesn’t support fake timer
• It doesn’t support fake HTTP server
Stub every dependency

• Native spy feature of Jasmine is not enough
• It doesn’t support fake timer
• It doesn’t support fake HTTP server
• It misuses Spy and Stub
Stub every dependency

• Native spy feature of Jasmine is not enough
• It doesn’t support fake timer
• It doesn’t support fake HTTP server
• It misuses Spy and Stub

   Sinon.js comes to rescue
Spy/Stub/Mock
Spy/Stub/Mock
•   A test spy is a function that records arguments, return value,
    the value of this and exception thrown (if any) for all its calls.
    A test spy can be an anonymous function or it can wrap an
    existing function.
Spy/Stub/Mock
•   A test spy is a function that records arguments, return value,
    the value of this and exception thrown (if any) for all its calls.
    A test spy can be an anonymous function or it can wrap an
    existing function.

•   Test stubs are functions (spies) with pre-programmed
    behavior. They support the full test spy API in addition to
    methods which can be used to alter the stub's behavior.
Spy/Stub/Mock
•   A test spy is a function that records arguments, return value,
    the value of this and exception thrown (if any) for all its calls.
    A test spy can be an anonymous function or it can wrap an
    existing function.

•   Test stubs are functions (spies) with pre-programmed
    behavior. They support the full test spy API in addition to
    methods which can be used to alter the stub's behavior.

•   Mocks (and mock expectations) are fake methods (like spies)
    with pre-programmed behavior (like stubs) as well as pre-
    programmed expectations. A mock will fail your test if it is not
    used as expected.
Spy/Stub/Mock
Spy/Stub/Mock


 Spy
Spy/Stub/Mock


 Spy



       Stub
Spy/Stub/Mock


 Spy



       Stub


              Mock
Spy
describe("when initialized", function () {
    beforeEach(function() {
        sinon.spy(this, "Flashcards");           create a spy for this.Flashcards
    })

      afterEach(function() {
          this.Flashcards.restore();
      })

      it("should throw Error when cultureCode is not passed", function() {
          try {
              var flashcards = new this.Flashcards([], {
              });
          } catch(e) {}
          expect(this.Flashcards.calledOnce()).toBeTruthy();
          expect(this.Flashcards.threw()).toBeTruthy();
          expect(this.Flashcards.calledWith([], {})).toBeTruthy();
      });
});
Spy
describe("when initialized", function () {
    beforeEach(function() {
        sinon.spy(this, "Flashcards");
    })

      afterEach(function() {
          this.Flashcards.restore();                unwraps the spy
      })

      it("should throw Error when cultureCode is not passed", function() {
          try {
              var flashcards = new this.Flashcards([], {
              });
          } catch(e) {}
          expect(this.Flashcards.calledOnce()).toBeTruthy();
          expect(this.Flashcards.threw()).toBeTruthy();
          expect(this.Flashcards.calledWith([], {})).toBeTruthy();
      });
});
Spy
describe("when initialized", function () {
    beforeEach(function() {
        sinon.spy(this, "Flashcards");
    })

      afterEach(function() {
          this.Flashcards.restore();
      })

      it("should throw Error when cultureCode is not passed", function() {
          try {
              var flashcards = new this.Flashcards([], {
              });
          } catch(e) {}
          expect(this.Flashcards.calledOnce()).toBeTruthy();     spy was called once
          expect(this.Flashcards.threw()).toBeTruthy();
          expect(this.Flashcards.calledWith([], {})).toBeTruthy();
      });
});
Spy
describe("when initialized", function () {
    beforeEach(function() {
        sinon.spy(this, "Flashcards");
    })

      afterEach(function() {
          this.Flashcards.restore();
      })

      it("should throw Error when cultureCode is not passed", function() {
          try {
              var flashcards = new this.Flashcards([], {
              });
          } catch(e) {}
          expect(this.Flashcards.calledOnce()).toBeTruthy();
          expect(this.Flashcards.threw()).toBeTruthy();
          expect(this.Flashcards.calledWith([], {})).toBeTruthy();
      });
});
                                   spy threw exception at least once
Spy
describe("when initialized", function () {
    beforeEach(function() {
        sinon.spy(this, "Flashcards");
    })

      afterEach(function() {
          this.Flashcards.restore();
      })

      it("should throw Error when cultureCode is not passed", function() {
          try {
              var flashcards = new this.Flashcards([], {
              });
          } catch(e) {}
          expect(this.Flashcards.calledOnce()).toBeTruthy();
          expect(this.Flashcards.threw()).toBeTruthy();
          expect(this.Flashcards.calledWith([], {})).toBeTruthy();
      });
});                                    spy was called at least once with
                                             provided argument
Stub
describe("when update model", function () {
    beforeEach(function () {
        this.sync = sinon.stub(Backbone, "sync");
    });

      afterEach(function () {                     create a stub for this.Flashcards
          this.sync.restore();
      });

    it("should put IsWordIKnow and IsWord properties at the end of url property",
function () {
        this.flashcard.set('IsWordIKnow', true);

        var expected_url = this.flashcard.url() + '&IsWordIKnow=' +
this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord');

            this.flashcard.save();

            expect(this.sync.getCall(0).args[2].url).toEqual(expected_url);
      });
});
Stub
describe("when update model", function () {
    beforeEach(function () {
        this.sync = sinon.stub(Backbone, "sync");
    });

      afterEach(function () {
          this.sync.restore();
      });

    it("should put IsWordIKnow and IsWord properties at the end of url property",
function () {
        this.flashcard.set('IsWordIKnow', true);

        var expected_url = this.flashcard.url() + '&IsWordIKnow=' +
this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord');

            this.flashcard.save();

            expect(this.sync.getCall(0).args[2].url).toEqual(expected_url);
      });
});
                  save method will invoke Backbone.sync that already stubbed
Stub
describe("when update model", function () {
    beforeEach(function () {
        this.sync = sinon.stub(Backbone, "sync");
    });

      afterEach(function () {
          this.sync.restore();
      });

    it("should put IsWordIKnow and IsWord properties at the end of url property",
function () {
        this.flashcard.set('IsWordIKnow', true);

        var expected_url = this.flashcard.url() + '&IsWordIKnow=' +
this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord');

            this.flashcard.save();

            expect(this.sync.getCall(0).args[2].url).toEqual(expected_url);
      });
});
             original sync method won’t be called if it was stubbed
             So, no ajax request won’t be fired
Stub
describe("when update model", function () {
    beforeEach(function () {
        this.sync = sinon.stub(Backbone, "sync");
    });

      afterEach(function () {
          this.sync.restore();
      });

    it("should put IsWordIKnow and IsWord properties at the end of url property",
function () {
        this.flashcard.set('IsWordIKnow', true);

        var expected_url = this.flashcard.url() + '&IsWordIKnow=' +
this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord');

            this.flashcard.save();

            expect(this.sync.getCall(0).args[2].url).toEqual(expected_url);
      });
});

             get arguments of stubbed calling (spy can also do this)
Stub
              But, Spy cannot do like so:

it("should always confirm every confirmation", function () {
    sinon.stub(window, 'confirm');
    confirm.returns(true);
    expect(confirm('Are you sure?')).toBeTruthy();
    window.confirm.restore();
}

      Makes window.confirm() return truth
Stub
              But, Spy cannot do like so:

it("should always confirm every confirmation", function () {
    sinon.stub(window, 'confirm');
    confirm.returns(true);
    expect(confirm('Are you sure?')).toBeTruthy();
    window.confirm.restore();
}

      Native confirm behavior won’t fired
Stub
              But, Spy cannot do like so:

it("should always confirm every confirmation", function () {
    sinon.stub(window, 'confirm');
    confirm.returns(true);
    expect(confirm('Are you sure?')).toBeTruthy();
    window.confirm.restore();
}

      Native confirm behavior won’t fired




  It needs user interaction to finish this test without stub.
   This test probably be failed if user not confirms with it.
                  That test case is unstable.
Mock

• Mock focuses on implementation details of
  one method
• It utilizes upfront expectation to verify details
  rather than asserting after the details
Mock
describe("when initialized", function () {
    it("should throw Error when cultureCode is not passed", function() {
        var myAPI = { method: function () {} };

            var spy = sinon.spy();
            var mock = sinon.mock(myAPI);
            mock.expects("method").once();         expectation upfront
            myAPI.method();
            spy();
                                      verify mock behavior
            mock.verify();
            expect(spy.calledOnce).toBeTruthy();
      });
});
Without FakeTimers
it("should show teacher box after 1 hour", function () {
    var hour = 1000 * 60 * 60;
    setTimeout(showTeacherbox, hour);

      waits(hour);         must wait 1 hour...crazy
      runs(function() {
          expect($(‘#teacherbox’)).toBeVisible();
      });
});
FakeTimers
it("should show teacher box after 1 hour", function () {
    this.clock = sinon.useFakeTimers();
    var hour = 1000 * 60 * 60;
                                                create a fake timer
      setTimeout(showTeacherbox, hour);

      this.clock.tick(hour);
      expect($(‘#teacherbox’)).toBeVisible();

      this.clock.restore();
});
FakeTimers
it("should show teacher box after 1 hour", function () {
    this.clock = sinon.useFakeTimers();
    var hour = 1000 * 60 * 60;

      setTimeout(showTeacherbox, hour);
                                           tick the clock ahead 1 hour
      this.clock.tick(hour);
      expect($(‘#teacherbox’)).toBeVisible();

      this.clock.restore();
});
FakeTimers
it("should show teacher box after 1 hour", function () {
    this.clock = sinon.useFakeTimers();
    var hour = 1000 * 60 * 60;

      setTimeout(showTeacherbox, hour);

      this.clock.tick(hour - 1);
      expect($(‘#teacherbox’)).toBeHidden();    won’t happen

      this.clock.tick(1);
      expect($(‘#teacherbox’)).toBeVisible();    will happen

      this.clock.restore();
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {                               Fake server stub XHR
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });                                          responds to given   URL and HTTP method
            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                                                                    given HTTP method
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                                                                          given URL
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },      fake response header
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );                                                 fake response body
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();
                                              immediately     responds with fake data

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);
            });
      });
});
Fake Server
describe("Flashcard model", function () {
    beforeEach(function () {
        this.Flashcard = require('models/Flashcard');
    });

      describe(“when fetch”, function() {
          beforeEach(function() {
              this.server = sinon.fakeServer.create();
              this.flashcard = new this.Flashcard({contentId: 174087});
          });
          afterEach(function() {
               this.server.restore();
          });

            it(“should get data from backend”, function() {
               this.server.respondWith(
                   "GET",
                   "/community/dailylesson/wordiknowupdate.ashx?contentId=174087",
                   [200,
                    { "Content-Type": "application/json" },
                    '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true,
              "ContentId": 174087, "IsWordIKnow": false }'
                  ]
              );
               this.flashcard.fetch();
               this.server.respond();

                  expect(this.flashcard.get(‘Value’)).toEqual(‘here’);    verify changes
            });
      });
});
Next Step
• fixture management
 • DOM
 • HTTP Response
• Code Coverage for Javascript
• Integration with CI
• Integration with jsTestDriver
Client Side Unit Testing
Thank you
Thank you

if you like this topic please give me
Thank you

if you like this topic please give me

More Related Content

PPS
Unit Testing
KEY
Unit Testing Your Application
PPTX
Unit Testing in Action - C#, NUnit, and Moq
PDF
Agile Programming Systems # TDD intro
PDF
Behavior Driven Development with SpecFlow
PPTX
Software Quality via Unit Testing
PDF
When develpment met test(shift left testing)
PPTX
Design for Testability
Unit Testing
Unit Testing Your Application
Unit Testing in Action - C#, NUnit, and Moq
Agile Programming Systems # TDD intro
Behavior Driven Development with SpecFlow
Software Quality via Unit Testing
When develpment met test(shift left testing)
Design for Testability

What's hot (20)

PPTX
TDD - Agile
PDF
Writing Testable Code
PPTX
Tools for Software Testing
PPTX
Unit tests benefits
PPT
Unit testing
PPTX
Unit tests & TDD
PDF
Win at life with unit testing
PPTX
Unit Testing (C#)
PPTX
Test-Driven Development
PPTX
Benefit From Unit Testing In The Real World
KEY
Introduction to Acceptance Test Driven Development
PDF
Design For Testability
PPTX
An Introduction to Unit Testing
PPTX
Unit Testing
PDF
An Introduction to Unit Test Using NUnit
PPTX
Unit Tests And Automated Testing
PDF
Introduction to Test Automation
PPTX
Practical unit testing in c & c++
PDF
Hands-on Experience Model based testing with spec explorer
TDD - Agile
Writing Testable Code
Tools for Software Testing
Unit tests benefits
Unit testing
Unit tests & TDD
Win at life with unit testing
Unit Testing (C#)
Test-Driven Development
Benefit From Unit Testing In The Real World
Introduction to Acceptance Test Driven Development
Design For Testability
An Introduction to Unit Testing
Unit Testing
An Introduction to Unit Test Using NUnit
Unit Tests And Automated Testing
Introduction to Test Automation
Practical unit testing in c & c++
Hands-on Experience Model based testing with spec explorer
Ad

Viewers also liked (6)

PPT
960Grid 实践
PPT
iCast2 广告代码库
PPTX
UNIT TESTING PPT
PDF
Lightning Talk #9: How UX and Data Storytelling Can Shape Policy by Mika Aldaba
PDF
SEO: Getting Personal
PDF
Succession “Losers”: What Happens to Executives Passed Over for the CEO Job?
960Grid 实践
iCast2 广告代码库
UNIT TESTING PPT
Lightning Talk #9: How UX and Data Storytelling Can Shape Policy by Mika Aldaba
SEO: Getting Personal
Succession “Losers”: What Happens to Executives Passed Over for the CEO Job?
Ad

Similar to Client Side Unit Testing (20)

PDF
Unit testing - An introduction
PDF
Unit, Integration, and Functional Testing_ 4 main points of difference.pdf
ZIP
Unit Testing in Java
PDF
Test driven development
PPS
Why Unit Testingl
PPS
Why Unit Testingl
PPS
Why unit testingl
PDF
What Is Unit Testing_ A Complete Guide With Examples.pdf
PDF
What Is Unit Testing A Complete Guide With Examples.pdf
PDF
How Unit Testing Strengthens Software Reliability
PDF
Agile Testing
PPT
Automated testing overview
PPTX
Testing 101
PDF
What is Unit Testing_ - A Complete Guide.pdf
PDF
DSR Testing (Part 1)
PPT
Automated+Testing+Vs+Manual+Testing
PPT
Automated Testing vs Manual Testing
PPT
Automated Testing vs Manual Testing
PPTX
1.1 Chapter_22_ Unit Testing-testing (1).pptx
PDF
What is Unit Testing? - A Complete Guide
Unit testing - An introduction
Unit, Integration, and Functional Testing_ 4 main points of difference.pdf
Unit Testing in Java
Test driven development
Why Unit Testingl
Why Unit Testingl
Why unit testingl
What Is Unit Testing_ A Complete Guide With Examples.pdf
What Is Unit Testing A Complete Guide With Examples.pdf
How Unit Testing Strengthens Software Reliability
Agile Testing
Automated testing overview
Testing 101
What is Unit Testing_ - A Complete Guide.pdf
DSR Testing (Part 1)
Automated+Testing+Vs+Manual+Testing
Automated Testing vs Manual Testing
Automated Testing vs Manual Testing
1.1 Chapter_22_ Unit Testing-testing (1).pptx
What is Unit Testing? - A Complete Guide

Recently uploaded (20)

PPTX
Cloud computing and distributed systems.
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PDF
Empathic Computing: Creating Shared Understanding
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Encapsulation_ Review paper, used for researhc scholars
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PDF
Electronic commerce courselecture one. Pdf
PDF
Unlocking AI with Model Context Protocol (MCP)
PPT
Teaching material agriculture food technology
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
PPTX
Understanding_Digital_Forensics_Presentation.pptx
DOCX
The AUB Centre for AI in Media Proposal.docx
PDF
Network Security Unit 5.pdf for BCA BBA.
PDF
cuic standard and advanced reporting.pdf
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
Cloud computing and distributed systems.
Digital-Transformation-Roadmap-for-Companies.pptx
Reach Out and Touch Someone: Haptics and Empathic Computing
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
Empathic Computing: Creating Shared Understanding
The Rise and Fall of 3GPP – Time for a Sabbatical?
Encapsulation_ Review paper, used for researhc scholars
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
20250228 LYD VKU AI Blended-Learning.pptx
Electronic commerce courselecture one. Pdf
Unlocking AI with Model Context Protocol (MCP)
Teaching material agriculture food technology
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
Understanding_Digital_Forensics_Presentation.pptx
The AUB Centre for AI in Media Proposal.docx
Network Security Unit 5.pdf for BCA BBA.
cuic standard and advanced reporting.pdf
Per capita expenditure prediction using model stacking based on satellite ima...

Client Side Unit Testing

  • 1. Client-Side Unit Testing Cloud Chen 2012/5/11
  • 3. You know you should, but you don’t
  • 4. You won’t be blamed for it
  • 7. There are many common issues that prevent developers for writing tests
  • 9. You think • Tests are not necessary and irrelevant
  • 10. You think • Tests are not necessary and irrelevant • Manual testing is enough
  • 11. You think • Tests are not necessary and irrelevant • Manual testing is enough • Lacking of specification
  • 12. You think • Tests are not necessary and irrelevant • Manual testing is enough • Lacking of specification • Spaghetti code are hard for testing
  • 13. You think • Tests are not necessary and irrelevant • Manual testing is enough • Lacking of specification • Spaghetti code are hard for testing • Web application needs functional testing rather than unit testing
  • 14. You think • Tests are not necessary and irrelevant • Manual testing is enough • Lacking of specification • Spaghetti code are hard for testing • Web application needs functional testing rather than unit testing • Unit testing looks like only useful for back-end code rather than front-end
  • 15. You think • Tests are not necessary and irrelevant • Manual testing is enough • Lacking of specification • Spaghetti code are hard for testing • Web application needs functional testing rather than unit testing • Unit testing looks like only useful for back-end code rather than front-end • Lazy...
  • 17. Our goal Help you start writing unit tests for front-end code
  • 19. Let’s start with another type of testing
  • 21. What is functional testing Functional testing is a type of black box testing that bases its test cases on the specifications of the software component under test. Functions are tested by feeding them input and examining the output, and internal program structure is rarely considered.
  • 22. What is functional testing Functional testing is a type of black box testing that bases its test cases on the specifications of the software component under test. Functions are tested by feeding them input and examining the output, and internal program structure is rarely considered.
  • 24. In a nutshell • Written from user perspective
  • 25. In a nutshell • Written from user perspective • Proving users are able to reproduce defined steps
  • 26. In a nutshell • Written from user perspective • Proving users are able to reproduce defined steps • Don’t need to consider internal program structure
  • 27. In a nutshell • Written from user perspective • Proving users are able to reproduce defined steps • Don’t need to consider internal program structure • Automating manual testing
  • 29. What is unit testing unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine if they are fit for use.
  • 30. What is unit testing unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine if they are fit for use.
  • 32. In a nutshell • Unit testing is complete isolation
  • 33. In a nutshell • Unit testing is complete isolation • Must irrelevant to external dependencies
  • 34. In a nutshell • Unit testing is complete isolation • Must irrelevant to external dependencies • All unreliable or slow dependencies of a tested unit should be stubbed
  • 35. In a nutshell • Unit testing is complete isolation • Must irrelevant to external dependencies • All unreliable or slow dependencies of a tested unit should be stubbed • Only the logic of that single unit is exercised
  • 36. In a nutshell • Unit testing is complete isolation • Must irrelevant to external dependencies • All unreliable or slow dependencies of a tested unit should be stubbed • Only the logic of that single unit is exercised • Must be fast
  • 37. What are differences between functional and unit testing
  • 38. What are differences between functional and unit testing unit functional perspective programmer user every module every process goal works as expected works as expected all cases coverage most user cases even edge cases result programmer :) user :)
  • 39. What are differences between functional and unit testing unit functional perspective programmer user every module every process goal works as expected works as expected all cases coverage most user cases even edge cases result programmer :) user :)
  • 40. What are differences between functional and unit testing unit functional perspective programmer user every module every process goal works as expected works as expected all cases coverage most user cases even edge cases result programmer :) user :)
  • 41. What are differences between functional and unit testing unit functional perspective programmer user every module every process goal works as expected works as expected all cases coverage most user cases even edge cases result programmer :) user :)
  • 42. What are differences between functional and unit testing unit functional perspective programmer user every module every process goal works as expected works as expected all cases coverage most user cases even edge cases result programmer :) user :)
  • 44. Hence.. • We are programmer
  • 45. Hence.. • We are programmer • We care our code quality
  • 46. Hence.. • We are programmer • We care our code quality • We should write unit test code
  • 47. Why, When of unit testing
  • 48. Why need unit testing
  • 49. Why need unit testing • Ensuring every component is bug-free
  • 50. Why need unit testing • Ensuring every component is bug-free • Ensuring every component is easy to modify
  • 51. Why need unit testing • Ensuring every component is bug-free • Ensuring every component is easy to modify • Easy to locating bug of complex logic
  • 52. Why need unit testing • Ensuring every component is bug-free • Ensuring every component is easy to modify • Easy to locating bug of complex logic • Preventing and capturing regression bug
  • 53. Why need unit testing • Ensuring every component is bug-free • Ensuring every component is easy to modify • Easy to locating bug of complex logic • Preventing and capturing regression bug • Testable code must be high readability and maintainability code
  • 54. Why need unit testing • Ensuring every component is bug-free • Ensuring every component is easy to modify • Easy to locating bug of complex logic • Preventing and capturing regression bug • Testable code must be high readability and maintainability code • and so on...
  • 55. When to do unit testing
  • 56. When to do unit testing • When you write your own classes, modules, libraries, frameworks.
  • 57. When to do unit testing • When you write your own classes, modules, libraries, frameworks. • You don’t need to write test code for fundamental framework
  • 59. testing style comparison TDD BDD based on function oriented feature oriented syntax testing language idiomatic spring from programmer stakeholder || PO accumulation test as code test as documentation
  • 60. testing style comparison TDD BDD based on function oriented feature oriented syntax testing language idiomatic spring from programmer stakeholder || PO accumulation test as code test as documentation
  • 61. testing style comparison TDD BDD based on function oriented feature oriented syntax testing language idiomatic spring from programmer stakeholder || PO accumulation test as code test as documentation
  • 62. testing style comparison TDD BDD based on function oriented feature oriented syntax testing language idiomatic spring from programmer stakeholder || PO accumulation test as code test as documentation
  • 63. testing style comparison TDD BDD based on function oriented feature oriented syntax testing language idiomatic spring from programmer stakeholder || PO accumulation test as code test as documentation
  • 65. testing style comparison • It doesn’t matter which style you choose
  • 66. testing style comparison • It doesn’t matter which style you choose • It does matter how many cases you have
  • 68. testing style comparison As we are using Scrum,
  • 69. testing style comparison As we are using Scrum, BDD is more suitable for us.
  • 71. BDD Framework • Focuses on assertion, doesn’t depend on DOM
  • 72. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API
  • 73. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy
  • 74. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper
  • 75. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper • Be able to extend matcher
  • 76. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper • Be able to extend matcher • Be able to run in several environments
  • 77. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper • Be able to extend matcher • Be able to run in several environments 1. Browser
  • 78. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper • Be able to extend matcher • Be able to run in several environments 1. Browser 2. CI
  • 79. BDD Framework • Focuses on assertion, doesn’t depend on DOM • Succinct API • Natively support Spy • Support spec helper • Be able to extend matcher • Be able to run in several environments 1. Browser 2. CI 3. NodeJS
  • 80. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 81. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); Suite }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 82. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); afterEach(function () { Inner Suite }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 83. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, }); "IsWordIKnow": false Spec }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 84. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); Expectation }); });
  • 85. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); }); Matcher
  • 86. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); Setup }); afterEach(function () { }); describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 87. Typical Spec & Succinct API describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); Setup }); afterEach(function () { }); Tear down describe(“when initialized”, function() { beforeEach(function() { this.flashcard = new this.Flashcard({ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }); }); it(“composite key should be composed by ContentId and IsWord properties”, function() { var expected_url = this.flashcard.get('ContentId') + '-' + this.flashcard.get('IsWord'); expect(this.flashcard.id).toEqual(expected_url); }); });
  • 90. Spec Helper beforeEach(function() { this.fixtures = { Flashcard: { valid: { // response starts here "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }, error: { // response starts here "ErrorCode": "101", "IsSuccess": "false" } } }
  • 91. Spec Helper beforeEach(function() { this.fixtures = { Flashcard: { valid: { // response starts here "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }, error: { // response starts here "ErrorCode": "101", "IsSuccess": "false" } } } Using this.fixtures.Flashcard.valid to access pre-defined fixture for testing when this spec file is included in your spec runner.
  • 92. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); });
  • 93. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); spy on instance method this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); });
  • 94. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); original function won’t be called it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); });
  • 95. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); match spied function was called });
  • 96. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); }); arguments of last call
  • 97. Spy describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when initialized”, function() { beforeEach(function() { spyOn(jQuery, ‘ajax’); this.flashcard = new this.Flashcard(); this.flashcard.fetch(); }); it(“should communicate with back-end via http get method”, function() { expect(jQuery.ajax).toHaveBeenCalled(); expect(jQuery.ajax.mostRecentCall.args[0].type).toEqual(‘GET’); }); }); }); arguments of last call type of first argument
  • 98. Extending Matcher <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy.
  • 99. Extending Matcher <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy. jasmine-jquery comes to rescue
  • 100. Matcher from jasmine jquery <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy.
  • 101. Matcher from jasmine jquery <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy. expect($(‘a’).toHaveClass(‘s-selected’);
  • 102. Matcher from jasmine jquery <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy. expect($(‘a’).toHaveClass(‘s-selected’); expectation failed:
  • 103. Matcher from jasmine jquery <a href=”#” class=”link”>this is a link</a> expect($(‘a’).hasClass('s-selected')).toBeTruthy(); expectation failed: Expected false to be truthy. expect($(‘a’).toHaveClass(‘s-selected’); expectation failed: Expected '<a></a>' to have class 's-selected'.
  • 104. jasmine-jquery features • a set of custom matchers for jQuery framework • an API for handling HTML fixtures in your specs
  • 105. jasmine-jquery matchers expect(x).toBeHidden() expect(x).toBeVisible() expect(x).toHaveAttr(attributeName, attributeValue) expect(x).toHaveProp(propertyName, propertyValue) expect(x).toHaveText(string) expect(x).toHaveHtml(string) expect(x).toHaveId(id) expect(x).toBeDisabled() expect(x).toBeFocused() expect(x).toHandle(eventName)
  • 106. jasmine-jquery fixtures In myfixture.html file: <div id="my-fixture">some complex content here</div> Inside your test: loadFixtures('myfixture.html'); $('#my-fixture').myTestedPlugin(); expect($('#my-fixture')).to...;
  • 107. Running Jasmine Standalone Runner * Manually manage of your project files and specs
  • 108. Running Jasmine Dedicated Server runs jasmine Ruby jasmine Gem * Using yaml manages of your project files and specs
  • 109. Recap rules of Unit Testing • Unit testing is complete isolation • Must irrelevant to external dependencies • All unreliable or slow dependencies of a tested unit should be stubbed • Only the logic of that single unit is exercised • Must be fast
  • 110. Recap rules of Unit Testing • Unit testing is complete isolation • Must irrelevant to external dependencies • All unreliable or slow dependencies of a tested unit should be stubbed • Only the logic of that single unit is exercised • Must be fast
  • 112. Stub every dependency • Native spy feature of Jasmine is not enough
  • 113. Stub every dependency • Native spy feature of Jasmine is not enough • It doesn’t support fake timer
  • 114. Stub every dependency • Native spy feature of Jasmine is not enough • It doesn’t support fake timer • It doesn’t support fake HTTP server
  • 115. Stub every dependency • Native spy feature of Jasmine is not enough • It doesn’t support fake timer • It doesn’t support fake HTTP server • It misuses Spy and Stub
  • 116. Stub every dependency • Native spy feature of Jasmine is not enough • It doesn’t support fake timer • It doesn’t support fake HTTP server • It misuses Spy and Stub Sinon.js comes to rescue
  • 118. Spy/Stub/Mock • A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.
  • 119. Spy/Stub/Mock • A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function. • Test stubs are functions (spies) with pre-programmed behavior. They support the full test spy API in addition to methods which can be used to alter the stub's behavior.
  • 120. Spy/Stub/Mock • A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function. • Test stubs are functions (spies) with pre-programmed behavior. They support the full test spy API in addition to methods which can be used to alter the stub's behavior. • Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre- programmed expectations. A mock will fail your test if it is not used as expected.
  • 124. Spy/Stub/Mock Spy Stub Mock
  • 125. Spy describe("when initialized", function () { beforeEach(function() { sinon.spy(this, "Flashcards"); create a spy for this.Flashcards }) afterEach(function() { this.Flashcards.restore(); }) it("should throw Error when cultureCode is not passed", function() { try { var flashcards = new this.Flashcards([], { }); } catch(e) {} expect(this.Flashcards.calledOnce()).toBeTruthy(); expect(this.Flashcards.threw()).toBeTruthy(); expect(this.Flashcards.calledWith([], {})).toBeTruthy(); }); });
  • 126. Spy describe("when initialized", function () { beforeEach(function() { sinon.spy(this, "Flashcards"); }) afterEach(function() { this.Flashcards.restore(); unwraps the spy }) it("should throw Error when cultureCode is not passed", function() { try { var flashcards = new this.Flashcards([], { }); } catch(e) {} expect(this.Flashcards.calledOnce()).toBeTruthy(); expect(this.Flashcards.threw()).toBeTruthy(); expect(this.Flashcards.calledWith([], {})).toBeTruthy(); }); });
  • 127. Spy describe("when initialized", function () { beforeEach(function() { sinon.spy(this, "Flashcards"); }) afterEach(function() { this.Flashcards.restore(); }) it("should throw Error when cultureCode is not passed", function() { try { var flashcards = new this.Flashcards([], { }); } catch(e) {} expect(this.Flashcards.calledOnce()).toBeTruthy(); spy was called once expect(this.Flashcards.threw()).toBeTruthy(); expect(this.Flashcards.calledWith([], {})).toBeTruthy(); }); });
  • 128. Spy describe("when initialized", function () { beforeEach(function() { sinon.spy(this, "Flashcards"); }) afterEach(function() { this.Flashcards.restore(); }) it("should throw Error when cultureCode is not passed", function() { try { var flashcards = new this.Flashcards([], { }); } catch(e) {} expect(this.Flashcards.calledOnce()).toBeTruthy(); expect(this.Flashcards.threw()).toBeTruthy(); expect(this.Flashcards.calledWith([], {})).toBeTruthy(); }); }); spy threw exception at least once
  • 129. Spy describe("when initialized", function () { beforeEach(function() { sinon.spy(this, "Flashcards"); }) afterEach(function() { this.Flashcards.restore(); }) it("should throw Error when cultureCode is not passed", function() { try { var flashcards = new this.Flashcards([], { }); } catch(e) {} expect(this.Flashcards.calledOnce()).toBeTruthy(); expect(this.Flashcards.threw()).toBeTruthy(); expect(this.Flashcards.calledWith([], {})).toBeTruthy(); }); }); spy was called at least once with provided argument
  • 130. Stub describe("when update model", function () { beforeEach(function () { this.sync = sinon.stub(Backbone, "sync"); }); afterEach(function () { create a stub for this.Flashcards this.sync.restore(); }); it("should put IsWordIKnow and IsWord properties at the end of url property", function () { this.flashcard.set('IsWordIKnow', true); var expected_url = this.flashcard.url() + '&IsWordIKnow=' + this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord'); this.flashcard.save(); expect(this.sync.getCall(0).args[2].url).toEqual(expected_url); }); });
  • 131. Stub describe("when update model", function () { beforeEach(function () { this.sync = sinon.stub(Backbone, "sync"); }); afterEach(function () { this.sync.restore(); }); it("should put IsWordIKnow and IsWord properties at the end of url property", function () { this.flashcard.set('IsWordIKnow', true); var expected_url = this.flashcard.url() + '&IsWordIKnow=' + this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord'); this.flashcard.save(); expect(this.sync.getCall(0).args[2].url).toEqual(expected_url); }); }); save method will invoke Backbone.sync that already stubbed
  • 132. Stub describe("when update model", function () { beforeEach(function () { this.sync = sinon.stub(Backbone, "sync"); }); afterEach(function () { this.sync.restore(); }); it("should put IsWordIKnow and IsWord properties at the end of url property", function () { this.flashcard.set('IsWordIKnow', true); var expected_url = this.flashcard.url() + '&IsWordIKnow=' + this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord'); this.flashcard.save(); expect(this.sync.getCall(0).args[2].url).toEqual(expected_url); }); }); original sync method won’t be called if it was stubbed So, no ajax request won’t be fired
  • 133. Stub describe("when update model", function () { beforeEach(function () { this.sync = sinon.stub(Backbone, "sync"); }); afterEach(function () { this.sync.restore(); }); it("should put IsWordIKnow and IsWord properties at the end of url property", function () { this.flashcard.set('IsWordIKnow', true); var expected_url = this.flashcard.url() + '&IsWordIKnow=' + this.flashcard.get('IsWordIKnow') + '&IsWord=' + this.flashcard.get('IsWord'); this.flashcard.save(); expect(this.sync.getCall(0).args[2].url).toEqual(expected_url); }); }); get arguments of stubbed calling (spy can also do this)
  • 134. Stub But, Spy cannot do like so: it("should always confirm every confirmation", function () { sinon.stub(window, 'confirm'); confirm.returns(true); expect(confirm('Are you sure?')).toBeTruthy(); window.confirm.restore(); } Makes window.confirm() return truth
  • 135. Stub But, Spy cannot do like so: it("should always confirm every confirmation", function () { sinon.stub(window, 'confirm'); confirm.returns(true); expect(confirm('Are you sure?')).toBeTruthy(); window.confirm.restore(); } Native confirm behavior won’t fired
  • 136. Stub But, Spy cannot do like so: it("should always confirm every confirmation", function () { sinon.stub(window, 'confirm'); confirm.returns(true); expect(confirm('Are you sure?')).toBeTruthy(); window.confirm.restore(); } Native confirm behavior won’t fired It needs user interaction to finish this test without stub. This test probably be failed if user not confirms with it. That test case is unstable.
  • 137. Mock • Mock focuses on implementation details of one method • It utilizes upfront expectation to verify details rather than asserting after the details
  • 138. Mock describe("when initialized", function () { it("should throw Error when cultureCode is not passed", function() { var myAPI = { method: function () {} }; var spy = sinon.spy(); var mock = sinon.mock(myAPI); mock.expects("method").once(); expectation upfront myAPI.method(); spy(); verify mock behavior mock.verify(); expect(spy.calledOnce).toBeTruthy(); }); });
  • 139. Without FakeTimers it("should show teacher box after 1 hour", function () { var hour = 1000 * 60 * 60; setTimeout(showTeacherbox, hour); waits(hour); must wait 1 hour...crazy runs(function() { expect($(‘#teacherbox’)).toBeVisible(); }); });
  • 140. FakeTimers it("should show teacher box after 1 hour", function () { this.clock = sinon.useFakeTimers(); var hour = 1000 * 60 * 60; create a fake timer setTimeout(showTeacherbox, hour); this.clock.tick(hour); expect($(‘#teacherbox’)).toBeVisible(); this.clock.restore(); });
  • 141. FakeTimers it("should show teacher box after 1 hour", function () { this.clock = sinon.useFakeTimers(); var hour = 1000 * 60 * 60; setTimeout(showTeacherbox, hour); tick the clock ahead 1 hour this.clock.tick(hour); expect($(‘#teacherbox’)).toBeVisible(); this.clock.restore(); });
  • 142. FakeTimers it("should show teacher box after 1 hour", function () { this.clock = sinon.useFakeTimers(); var hour = 1000 * 60 * 60; setTimeout(showTeacherbox, hour); this.clock.tick(hour - 1); expect($(‘#teacherbox’)).toBeHidden(); won’t happen this.clock.tick(1); expect($(‘#teacherbox’)).toBeVisible(); will happen this.clock.restore(); });
  • 143. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 144. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { Fake server stub XHR this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 145. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); responds to given URL and HTTP method it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 146. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( given HTTP method "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 147. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( given URL "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 148. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, fake response header '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 149. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); fake response body this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 150. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); immediately responds with fake data expect(this.flashcard.get(‘Value’)).toEqual(‘here’); }); }); });
  • 151. Fake Server describe("Flashcard model", function () { beforeEach(function () { this.Flashcard = require('models/Flashcard'); }); describe(“when fetch”, function() { beforeEach(function() { this.server = sinon.fakeServer.create(); this.flashcard = new this.Flashcard({contentId: 174087}); }); afterEach(function() { this.server.restore(); }); it(“should get data from backend”, function() { this.server.respondWith( "GET", "/community/dailylesson/wordiknowupdate.ashx?contentId=174087", [200, { "Content-Type": "application/json" }, '{ "Value": "here", "Translation": "这儿", "Audio": "here_en.mp3", "IsWord": true, "ContentId": 174087, "IsWordIKnow": false }' ] ); this.flashcard.fetch(); this.server.respond(); expect(this.flashcard.get(‘Value’)).toEqual(‘here’); verify changes }); }); });
  • 152. Next Step • fixture management • DOM • HTTP Response • Code Coverage for Javascript • Integration with CI • Integration with jsTestDriver
  • 155. Thank you if you like this topic please give me
  • 156. Thank you if you like this topic please give me

Editor's Notes

  • #2: \n
  • #3: \n
  • #4: \n
  • #5: \n
  • #6: \n
  • #7: \n
  • #8: \n
  • #9: \n
  • #10: \n
  • #11: \n
  • #12: \n
  • #13: \n
  • #14: \n
  • #15: \n
  • #16: \n
  • #17: &amp;#x518D;&amp;#x5F00;&amp;#x59CB;&amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x4E4B;&amp;#x524D;, &amp;#x5148;&amp;#x4E86;&amp;#x89E3;&amp;#x4E00;&amp;#x4E0B;&amp;#x53E6;&amp;#x5916;&amp;#x4E00;&amp;#x79CD;&amp;#x6D4B;&amp;#x8BD5;. &amp;#x8BA4;&amp;#x6E05;&amp;#x5404;&amp;#x79CD;&amp;#x7C7B;&amp;#x578B;&amp;#x6D4B;&amp;#x8BD5;&amp;#x4E4B;&amp;#x95F4;&amp;#x7684;&amp;#x5DEE;&amp;#x522B;\n
  • #18: \n
  • #19: \n
  • #20: \n
  • #21: &amp;#x4F7F;&amp;#x624B;&amp;#x52A8;&amp;#x6D4B;&amp;#x8BD5;&amp;#x81EA;&amp;#x52A8;&amp;#x5316;\n
  • #22: &amp;#x4F7F;&amp;#x624B;&amp;#x52A8;&amp;#x6D4B;&amp;#x8BD5;&amp;#x81EA;&amp;#x52A8;&amp;#x5316;\n
  • #23: &amp;#x4F7F;&amp;#x624B;&amp;#x52A8;&amp;#x6D4B;&amp;#x8BD5;&amp;#x81EA;&amp;#x52A8;&amp;#x5316;\n
  • #24: &amp;#x4F7F;&amp;#x624B;&amp;#x52A8;&amp;#x6D4B;&amp;#x8BD5;&amp;#x81EA;&amp;#x52A8;&amp;#x5316;\n
  • #25: \n
  • #26: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x662F;&amp;#x4E00;&amp;#x79CD;&amp;#x65B9;&amp;#x6CD5;, &amp;#x6E90;&amp;#x7801;&amp;#x4E2D;&amp;#x72EC;&amp;#x7ACB;&amp;#x7684;&amp;#x5355;&amp;#x5143;\n
  • #27: &amp;#x6CE8;&amp;#x610F;&amp;#x72EC;&amp;#x7ACB;&amp;#x7684;&amp;#x5355;&amp;#x5143;&amp;#x5E76;&amp;#x4E0D;&amp;#x4E00;&amp;#x5B9A;&amp;#x662F;&amp;#x5355;&amp;#x72EC;&amp;#x7684;&amp;#x65B9;&amp;#x6CD5;, \n
  • #28: &amp;#x5FC5;&amp;#x987B;&amp;#x548C;&amp;#x5916;&amp;#x90E8;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;&amp;#x4E0D;&amp;#x76F8;&amp;#x5173;, &amp;#x4FDD;&amp;#x6301;&amp;#x5355;&amp;#x5143;&amp;#x7684;&amp;#x72EC;&amp;#x7ACB;&amp;#x6027;, &amp;#x662F;&amp;#x4E3A;&amp;#x4E86;&amp;#x4FDD;&amp;#x8BC1;&amp;#x9519;&amp;#x8BEF;&amp;#x4E0D;&amp;#x662F;&amp;#x7531;&amp;#x5916;&amp;#x90E8;&amp;#x7684;&amp;#x4EE3;&amp;#x7801;&amp;#x5F15;&amp;#x8D77;&amp;#x7684;\n&amp;#x4E0D;&amp;#x7A33;&amp;#x5B9A;&amp;#x7684;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;, &amp;#x5982;HTTP Request, timer, slow animation, &amp;#x9700;&amp;#x8981;&amp;#x88AB;stubbed\n\n
  • #29: &amp;#x5FC5;&amp;#x987B;&amp;#x548C;&amp;#x5916;&amp;#x90E8;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;&amp;#x4E0D;&amp;#x76F8;&amp;#x5173;, &amp;#x4FDD;&amp;#x6301;&amp;#x5355;&amp;#x5143;&amp;#x7684;&amp;#x72EC;&amp;#x7ACB;&amp;#x6027;, &amp;#x662F;&amp;#x4E3A;&amp;#x4E86;&amp;#x4FDD;&amp;#x8BC1;&amp;#x9519;&amp;#x8BEF;&amp;#x4E0D;&amp;#x662F;&amp;#x7531;&amp;#x5916;&amp;#x90E8;&amp;#x7684;&amp;#x4EE3;&amp;#x7801;&amp;#x5F15;&amp;#x8D77;&amp;#x7684;\n&amp;#x4E0D;&amp;#x7A33;&amp;#x5B9A;&amp;#x7684;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;, &amp;#x5982;HTTP Request, timer, slow animation, &amp;#x9700;&amp;#x8981;&amp;#x88AB;stubbed\n\n
  • #30: &amp;#x5FC5;&amp;#x987B;&amp;#x548C;&amp;#x5916;&amp;#x90E8;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;&amp;#x4E0D;&amp;#x76F8;&amp;#x5173;, &amp;#x4FDD;&amp;#x6301;&amp;#x5355;&amp;#x5143;&amp;#x7684;&amp;#x72EC;&amp;#x7ACB;&amp;#x6027;, &amp;#x662F;&amp;#x4E3A;&amp;#x4E86;&amp;#x4FDD;&amp;#x8BC1;&amp;#x9519;&amp;#x8BEF;&amp;#x4E0D;&amp;#x662F;&amp;#x7531;&amp;#x5916;&amp;#x90E8;&amp;#x7684;&amp;#x4EE3;&amp;#x7801;&amp;#x5F15;&amp;#x8D77;&amp;#x7684;\n&amp;#x4E0D;&amp;#x7A33;&amp;#x5B9A;&amp;#x7684;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;, &amp;#x5982;HTTP Request, timer, slow animation, &amp;#x9700;&amp;#x8981;&amp;#x88AB;stubbed\n\n
  • #31: &amp;#x5FC5;&amp;#x987B;&amp;#x548C;&amp;#x5916;&amp;#x90E8;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;&amp;#x4E0D;&amp;#x76F8;&amp;#x5173;, &amp;#x4FDD;&amp;#x6301;&amp;#x5355;&amp;#x5143;&amp;#x7684;&amp;#x72EC;&amp;#x7ACB;&amp;#x6027;, &amp;#x662F;&amp;#x4E3A;&amp;#x4E86;&amp;#x4FDD;&amp;#x8BC1;&amp;#x9519;&amp;#x8BEF;&amp;#x4E0D;&amp;#x662F;&amp;#x7531;&amp;#x5916;&amp;#x90E8;&amp;#x7684;&amp;#x4EE3;&amp;#x7801;&amp;#x5F15;&amp;#x8D77;&amp;#x7684;\n&amp;#x4E0D;&amp;#x7A33;&amp;#x5B9A;&amp;#x7684;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;, &amp;#x5982;HTTP Request, timer, slow animation, &amp;#x9700;&amp;#x8981;&amp;#x88AB;stubbed\n\n
  • #32: &amp;#x5FC5;&amp;#x987B;&amp;#x548C;&amp;#x5916;&amp;#x90E8;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;&amp;#x4E0D;&amp;#x76F8;&amp;#x5173;, &amp;#x4FDD;&amp;#x6301;&amp;#x5355;&amp;#x5143;&amp;#x7684;&amp;#x72EC;&amp;#x7ACB;&amp;#x6027;, &amp;#x662F;&amp;#x4E3A;&amp;#x4E86;&amp;#x4FDD;&amp;#x8BC1;&amp;#x9519;&amp;#x8BEF;&amp;#x4E0D;&amp;#x662F;&amp;#x7531;&amp;#x5916;&amp;#x90E8;&amp;#x7684;&amp;#x4EE3;&amp;#x7801;&amp;#x5F15;&amp;#x8D77;&amp;#x7684;\n&amp;#x4E0D;&amp;#x7A33;&amp;#x5B9A;&amp;#x7684;&amp;#x4F9D;&amp;#x8D56;&amp;#x5173;&amp;#x7CFB;, &amp;#x5982;HTTP Request, timer, slow animation, &amp;#x9700;&amp;#x8981;&amp;#x88AB;stubbed\n\n
  • #33: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x53C8;&amp;#x79F0;&amp;#x6A21;&amp;#x5757;&amp;#x6D4B;&amp;#x8BD5;\n
  • #34: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x53C8;&amp;#x79F0;&amp;#x6A21;&amp;#x5757;&amp;#x6D4B;&amp;#x8BD5;\n
  • #35: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x53C8;&amp;#x79F0;&amp;#x6A21;&amp;#x5757;&amp;#x6D4B;&amp;#x8BD5;\n
  • #36: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x53C8;&amp;#x79F0;&amp;#x6A21;&amp;#x5757;&amp;#x6D4B;&amp;#x8BD5;\n
  • #37: &amp;#x5355;&amp;#x5143;&amp;#x6D4B;&amp;#x8BD5;&amp;#x53C8;&amp;#x79F0;&amp;#x6A21;&amp;#x5757;&amp;#x6D4B;&amp;#x8BD5;\n
  • #38: &amp;#x8FD9;&amp;#x91CC;&amp;#x5E76;&amp;#x4E0D;&amp;#x662F;&amp;#x8BF4;&amp;#x6211;&amp;#x4EEC;&amp;#x4E0D;&amp;#x9700;&amp;#x8981;&amp;#x5199;functional testing. Functional Testing&amp;#x662F;&amp;#x4E00;&amp;#x79CD;&amp;#x8865;&amp;#x5145;, &amp;#x548C;unit testing&amp;#x4E00;&amp;#x6837;&amp;#x5FC5;&amp;#x4E0D;&amp;#x53EF;&amp;#x7F3A;\n&amp;#x6211;&amp;#x4EEC;&amp;#x8FD9;&amp;#x91CC;&amp;#x53EA;focus unit testing\n
  • #39: &amp;#x8FD9;&amp;#x91CC;&amp;#x5E76;&amp;#x4E0D;&amp;#x662F;&amp;#x8BF4;&amp;#x6211;&amp;#x4EEC;&amp;#x4E0D;&amp;#x9700;&amp;#x8981;&amp;#x5199;functional testing. Functional Testing&amp;#x662F;&amp;#x4E00;&amp;#x79CD;&amp;#x8865;&amp;#x5145;, &amp;#x548C;unit testing&amp;#x4E00;&amp;#x6837;&amp;#x5FC5;&amp;#x4E0D;&amp;#x53EF;&amp;#x7F3A;\n&amp;#x6211;&amp;#x4EEC;&amp;#x8FD9;&amp;#x91CC;&amp;#x53EA;focus unit testing\n
  • #40: &amp;#x8FD9;&amp;#x91CC;&amp;#x5E76;&amp;#x4E0D;&amp;#x662F;&amp;#x8BF4;&amp;#x6211;&amp;#x4EEC;&amp;#x4E0D;&amp;#x9700;&amp;#x8981;&amp;#x5199;functional testing. Functional Testing&amp;#x662F;&amp;#x4E00;&amp;#x79CD;&amp;#x8865;&amp;#x5145;, &amp;#x548C;unit testing&amp;#x4E00;&amp;#x6837;&amp;#x5FC5;&amp;#x4E0D;&amp;#x53EF;&amp;#x7F3A;\n&amp;#x6211;&amp;#x4EEC;&amp;#x8FD9;&amp;#x91CC;&amp;#x53EA;focus unit testing\n
  • #41: \n
  • #42: \n
  • #43: \n
  • #44: \n
  • #45: \n
  • #46: \n
  • #47: \n
  • #48: \n
  • #49: \n
  • #50: &amp;#x9762;&amp;#x5411;&amp;#x51FD;&amp;#x6570;, &amp;#x5173;&amp;#x6CE8;&amp;#x4EE3;&amp;#x7801;&amp;#x5B9E;&amp;#x73B0;, &amp;#x6BD4;&amp;#x8F83;&amp;#x5728;&amp;#x610F;&amp;#x4EE3;&amp;#x7801;&amp;#x8986;&amp;#x76D6;&amp;#x7387; / &amp;#x9762;&amp;#x5411;&amp;#x529F;&amp;#x80FD;, &amp;#x5173;&amp;#x6CE8;&amp;#x4E1A;&amp;#x52A1;, &amp;#x7531;&amp;#x5916;&amp;#x53CA;&amp;#x5185;\n&amp;#x8F83;&amp;#x4E13;&amp;#x4E1A;&amp;#x5316;&amp;#x7684;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8BED;&amp;#x53E5; / &amp;#x7B26;&amp;#x5408;&amp;#x8BED;&amp;#x8A00;&amp;#x4E60;&amp;#x60EF;, &amp;#x63A5;&amp;#x8FD1;&amp;#x81EA;&amp;#x7136;&amp;#x8BED;&amp;#x8A00;\n
  • #51: &amp;#x9762;&amp;#x5411;&amp;#x51FD;&amp;#x6570;, &amp;#x5173;&amp;#x6CE8;&amp;#x4EE3;&amp;#x7801;&amp;#x5B9E;&amp;#x73B0;, &amp;#x6BD4;&amp;#x8F83;&amp;#x5728;&amp;#x610F;&amp;#x4EE3;&amp;#x7801;&amp;#x8986;&amp;#x76D6;&amp;#x7387; / &amp;#x9762;&amp;#x5411;&amp;#x529F;&amp;#x80FD;, &amp;#x5173;&amp;#x6CE8;&amp;#x4E1A;&amp;#x52A1;, &amp;#x7531;&amp;#x5916;&amp;#x53CA;&amp;#x5185;\n&amp;#x8F83;&amp;#x4E13;&amp;#x4E1A;&amp;#x5316;&amp;#x7684;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8BED;&amp;#x53E5; / &amp;#x7B26;&amp;#x5408;&amp;#x8BED;&amp;#x8A00;&amp;#x4E60;&amp;#x60EF;, &amp;#x63A5;&amp;#x8FD1;&amp;#x81EA;&amp;#x7136;&amp;#x8BED;&amp;#x8A00;\n
  • #52: &amp;#x9762;&amp;#x5411;&amp;#x51FD;&amp;#x6570;, &amp;#x5173;&amp;#x6CE8;&amp;#x4EE3;&amp;#x7801;&amp;#x5B9E;&amp;#x73B0;, &amp;#x6BD4;&amp;#x8F83;&amp;#x5728;&amp;#x610F;&amp;#x4EE3;&amp;#x7801;&amp;#x8986;&amp;#x76D6;&amp;#x7387; / &amp;#x9762;&amp;#x5411;&amp;#x529F;&amp;#x80FD;, &amp;#x5173;&amp;#x6CE8;&amp;#x4E1A;&amp;#x52A1;, &amp;#x7531;&amp;#x5916;&amp;#x53CA;&amp;#x5185;\n&amp;#x8F83;&amp;#x4E13;&amp;#x4E1A;&amp;#x5316;&amp;#x7684;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8BED;&amp;#x53E5; / &amp;#x7B26;&amp;#x5408;&amp;#x8BED;&amp;#x8A00;&amp;#x4E60;&amp;#x60EF;, &amp;#x63A5;&amp;#x8FD1;&amp;#x81EA;&amp;#x7136;&amp;#x8BED;&amp;#x8A00;\n
  • #53: &amp;#x9762;&amp;#x5411;&amp;#x51FD;&amp;#x6570;, &amp;#x5173;&amp;#x6CE8;&amp;#x4EE3;&amp;#x7801;&amp;#x5B9E;&amp;#x73B0;, &amp;#x6BD4;&amp;#x8F83;&amp;#x5728;&amp;#x610F;&amp;#x4EE3;&amp;#x7801;&amp;#x8986;&amp;#x76D6;&amp;#x7387; / &amp;#x9762;&amp;#x5411;&amp;#x529F;&amp;#x80FD;, &amp;#x5173;&amp;#x6CE8;&amp;#x4E1A;&amp;#x52A1;, &amp;#x7531;&amp;#x5916;&amp;#x53CA;&amp;#x5185;\n&amp;#x8F83;&amp;#x4E13;&amp;#x4E1A;&amp;#x5316;&amp;#x7684;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8BED;&amp;#x53E5; / &amp;#x7B26;&amp;#x5408;&amp;#x8BED;&amp;#x8A00;&amp;#x4E60;&amp;#x60EF;, &amp;#x63A5;&amp;#x8FD1;&amp;#x81EA;&amp;#x7136;&amp;#x8BED;&amp;#x8A00;\n
  • #54: &amp;#x9762;&amp;#x5411;&amp;#x51FD;&amp;#x6570;, &amp;#x5173;&amp;#x6CE8;&amp;#x4EE3;&amp;#x7801;&amp;#x5B9E;&amp;#x73B0;, &amp;#x6BD4;&amp;#x8F83;&amp;#x5728;&amp;#x610F;&amp;#x4EE3;&amp;#x7801;&amp;#x8986;&amp;#x76D6;&amp;#x7387; / &amp;#x9762;&amp;#x5411;&amp;#x529F;&amp;#x80FD;, &amp;#x5173;&amp;#x6CE8;&amp;#x4E1A;&amp;#x52A1;, &amp;#x7531;&amp;#x5916;&amp;#x53CA;&amp;#x5185;\n&amp;#x8F83;&amp;#x4E13;&amp;#x4E1A;&amp;#x5316;&amp;#x7684;&amp;#x6D4B;&amp;#x8BD5;&amp;#x8BED;&amp;#x53E5; / &amp;#x7B26;&amp;#x5408;&amp;#x8BED;&amp;#x8A00;&amp;#x4E60;&amp;#x60EF;, &amp;#x63A5;&amp;#x8FD1;&amp;#x81EA;&amp;#x7136;&amp;#x8BED;&amp;#x8A00;\n
  • #55: \n
  • #56: \n
  • #57: \n
  • #58: \n
  • #59: \n
  • #60: \n
  • #61: \n
  • #62: \n
  • #63: \n
  • #64: \n
  • #65: \n
  • #66: \n
  • #67: \n
  • #68: \n
  • #69: \n
  • #70: \n
  • #71: \n
  • #72: \n
  • #73: \n
  • #74: \n
  • #75: \n
  • #76: \n
  • #77: \n
  • #78: \n
  • #79: \n
  • #80: \n
  • #81: Jasmine&amp;#x81EA;&amp;#x5E26;&amp;#x7684;Spy&amp;#x884C;&amp;#x4E3A;&amp;#x6709;&amp;#x4E9B;&amp;#x5947;&amp;#x602A;, spy&amp;#x5E94;&amp;#x8BE5;&amp;#x8BBE;&amp;#x8BA1;&amp;#x4E3A;&amp;#x4E0D;&amp;#x963B;&amp;#x6B62;&amp;#x539F;&amp;#x59CB;&amp;#x7684;&amp;#x884C;&amp;#x4E3A;\n
  • #82: \n
  • #83: \n
  • #84: \n
  • #85: \n
  • #86: \n
  • #87: \n
  • #88: \n
  • #89: \n
  • #90: \n
  • #91: \n
  • #92: \n
  • #93: \n
  • #94: \n
  • #95: \n
  • #96: \n
  • #97: \n
  • #98: \n
  • #99: \n
  • #100: \n
  • #101: \n
  • #102: \n
  • #103: \n
  • #104: \n
  • #105: \n
  • #106: \n
  • #107: \n
  • #108: \n
  • #109: \n
  • #110: \n
  • #111: \n
  • #112: \n
  • #113: \n
  • #114: stub&amp;#x548C;spy&amp;#x7684;&amp;#x6700;&amp;#x5927;&amp;#x533A;&amp;#x522B;&amp;#x662F;stub&amp;#x540E;&amp;#x7684;&amp;#x65B9;&amp;#x6CD5;, &amp;#x539F;&amp;#x59CB;&amp;#x884C;&amp;#x4E3A;&amp;#x662F;&amp;#x4E0D;&amp;#x4F1A;&amp;#x88AB;&amp;#x6267;&amp;#x884C;&amp;#x7684;.\n
  • #115: \n
  • #116: \n
  • #117: stub&amp;#x53EF;&amp;#x4EE5;&amp;#x505A;&amp;#x66F4;&amp;#x591A;, &amp;#x4E0D;&amp;#x4EC5;&amp;#x4EC5;&amp;#x662F;&amp;#x8FD4;&amp;#x56DE;&amp;#x503C;, &amp;#x63A7;&amp;#x5236;&amp;#x56DE;&amp;#x8C03;&amp;#x51FD;&amp;#x6570;&amp;#x7684;&amp;#x53C2;&amp;#x6570;&amp;#x4E5F;&amp;#x662F;&amp;#x53EF;&amp;#x884C;&amp;#x7684;.\n
  • #118: \n
  • #119: \n
  • #120: \n
  • #121: \n
  • #122: \n
  • #123: \n
  • #124: \n
  • #125: \n
  • #126: \n
  • #127: \n
  • #128: \n
  • #129: \n
  • #130: \n
  • #131: \n
  • #132: \n
  • #133: \n
  • #134: \n
  • #135: \n
  • #136: \n