0

In opening I would like to say; I'm pretty new for programing. I'm working with JS for a year. Using Sencha's ExtJS and usign Bryntum's Siesta test tool for unit and integration(ui) tests.

Right now developing an application through Mastering Ext JS book. I've started to write tests only for a couple months. First of all read a lot about those topics;

well.. I keep going well and just finished the first portion of ui-test for my app =)and now sailing to unit-tests. The thing is still i'm not sure what to test/how to test for unit-test! So I would like to share a couple of code snippets and asking for your advice.


The project which code snippet will be sharing is build with ExtJS 6.5; a Globals.js file has been defined which aims to working through whole app.

Globals.js

Ext.define('MyApp.Globals', {
    singleton: true,

    ...       

    config: {
        webFont: 'FontAwesome',
        add: 'xf067',
        edit: 'xf040',
        delete: 'xf1f8',
        save: 'xf00c',
        cancel: 'xf0e2'
    },

    STATUS_FUTURE: 'A',
    STATUS_EXPECTED: 'E',
    STATUS_CANCEL: 'X',

    status: {
        'A': "Future",
        'E': "Expected",
        'X': "Cancelled",
    },

    constructor: function (config) {
        this.initConfig(config);
    },

    getStatusDesc: function (status) {  // First method!
        var me = this;

        if (me.isEmptyStr(status)) {
            status = me.STATUS_FUTURE;
        }

        return me.status[status];
    },

    getGlyph: function (glyph) { // Second method!
        var me = this;
        var font = me.getWebFont();

        if (typeof me.config[glyph] === 'undefined') {
            return false;
        }

        return me.config[glyph] + '@' + font;
    },

    isEmptyStr: function (aStr) { // Third method!
        return Ext.isEmpty(aStr);
    },

    .....

and some unit-test sample I've wrote till now;

describe('MyApp Globals \'typeof\' & \'return\' unit-test', function(t) {

    t.ok(MyApp.Globals, 'Found Globals.js');

    t.describe('getStatusDesc() method', function(t) {

        t.it('should be a function', function(t) {
            t.expect(typeof MyApp.Globals.getStatusDesc).toEqual('function');
        });

        t.it('should return a string', function(t) {
            t.expect(typeof MyApp.Globals.getStatusDesc()).toEqual('string');
        });
    });

    t.describe('getGlyph() method', function(t) {

        t.it('should be a function', function(t) {
            t.expect(typeof MyApp.Globals.getGlyph).toEqual('function');
        });

        t.it('should return a boolean', function(t) {
            t.expect(typeof MyApp.Globals.getGlyph()).toEqual('boolean'); //I've expected it will return as string but didn't not! and passed as green =|
        });
    });

    t.describe('isEmptyStr() method', function(t) {

        t.it('should be a function', function(t) {
            t.expect(typeof MyApp.Globals.isEmptyStr).toEqual('function');
        });

        t.it('should return a boolean', function(t) {
            t.expect(typeof MyApp.Globals.isEmptyStr()).toEqual('boolean');
        });
    });

as you see; there has been three method defined (getStatusDesc, getGlyph, isEmptyStr) and I would like to walk through one-by-one to show you where/which methods has been effected with those three method.

1. getStatusDesc() has usage in Grid.js;

renderStatusCol: function (value, meta, record) {
        ... //Some code on here

        return ... + MyApp.Globals.getStatusDesc(value) + ...;
},

2. getGlyph() has usage in Data.js;

getEditForm: function () {
    return [
        {
            xtype: 'statusedits',
            glyph: MyApp.Globals.getGlyph('edit')
        }
    ]
},

3. isEmptyStr() has usage in loginAuth.js (checks if any string type data returns within json);

tokenValidate: function () {
    var me = this;

    return !MyApp.Globals.isEmptyStr(me.tokenRead()) && !MyApp.Globals.dateIsPassed(me.tokenExpire());
},

So here will be my question;

  1. What I should to do more for unit-test through those sample methods above?
  2. Should i looking only for return as data-type or as expected value? (I mean for example getGlyph method returns as boolean type but aims to check glyph's shortcode)
  3. How i should planing for a robust unit-test structure/design?

Could you give ideas within sample code snippets. Thanks a lot!


UPDATE

New unit test snippet;

describe('MyApp Globals unit-test', function(t) {

    t.ok(MyApp.Globals, 'Found Globals.js');

    t.describe('getStatusDesc() method', function(t) {
        t.it('should return \'A\' as status description', function(t) {
            t.expect(MyApp.Globals.getStatusDesc('A')).toEqual('Future'); //Result: DONE! Expect "Future" to be equal to "Future" 
        });
    });

    t.describe('getGlyph() method', function(t) {
        t.it('should return \'add\' ass webfont-code', function(t) {
            t.expect(MyApp.Globals.getGlyph('add')).toEqual('xf067@FontAwesome'); //Result: DONE! Expect "xf067@FontAwesome" to be equal to "xf067@FontAwesome"
        });
    });

    t.describe('isEmptyStr() method', function(t) {
        t.it('should be false when it has string inside', function(t) {
            t.expect(MyApp.Globals.isEmptyStr('somestring')).toEqual(false); //Result: DONE! Expect false to be equal to false 
        });
    });
  • 2
    I don't think close votes and downvotes are necessary here. The question shows a misunderstanding of what unit testing is about, and invites a straightforward answer; it doesn't seem too broad to me. – Arseni Mourzenko Dec 29 '17 at 10:36
3

There is no need to test that a function is a function or that it returns a string.

What you need to test is that, for instance, when calling isEmptyStr and passing to it an empty string, it returns true, and when calling it with a string Hello, World!, is returns false.

In other words, unit tests should focus on the contents of the method. Your test that something is a function doesn't concern the contents, but the signature. There is little sense to do that. Both of the cases you're testing will also be checked implicitly in a test which focuses on the contents. If, in a test, I call ƒ, checking that ƒ(x) returns a string y, the test will fail if ƒ is not a function, and it will also fail if ƒ returns a non-string.

What I should to do more for unit-test through those sample methods above?

You should test all your business logic. For the moment, you tested only the definitions, which is unecessary.

Should i looking only for return as data-type or as expected value?

You need to check the expected value. Incidentally, it would also check for its type (if you do write your test correctly; in JavaScript, == is a wrong operator for that, as it is in most of the cases anyway, while === is the right one).

How i should planing for a robust unit-test structure/design?

Unit testing is white-box testing. This means that if the code is already written, you look at your code, check the different execution paths, are write tests based on that. A better alternative is to do test-driven development. There are a lot of books and articles on the subject; to start, check two authors: Kent Beck and Martin Fowler.

Could you give ideas within sample code snippets.

Here's one.

Sample code

Step 1

Requirements:

  1. The application should greet a user, by saying “Hello, Jeff!” where Jeff is the name of the user.

  2. Some users may be guests. In this case, they have no name (javaScript's undefined value). The app should still greet them by saying “Hello, guest!”

  3. In some cases, the name appears to be an empty string. In this case, the application should simply say “Hello!”

Based on those requirements, you can write the first test:

t.it('should greet a user', (t) => {
    const actual = app.sayHello('Jeff');
    t.expect(actual).toBe('Hello, Jeff!');
});

Check that the test failed. It's time to start writing some code. Let's be as lazy as possible, implementing the most basic piece of code which would make the test pass.

const sayHello = _ => 'Hello, Jeff!';

That works. Next test please.

t.it('should greet a user, calling him by his name', (t) => {
    const actual = app.sayHello('Cindy');
    t.expect(actual).toBe('Hello, Cindy!');
});

Hey, this one fails, because we expect Hello, Cindy! as a result, but got Hello, Jeff! instead. Let's change our code.

const sayHello = name => `Hello, ${name}!`;

Nice, the second test passes. Do we need to test it for Joshua, Michael and Suzy? Not really. If you create just another test with a different name, the test will pass, before even you change the code. This indicates that you don't need the test. One fundamental rule of test-driven development is that any new test should fail first, and only after that you should start modifying your code to make it pass.

However, what about an undefined value?

t.it('should greet a guest', (t) => {
    const actual = app.sayHello();
    t.expect(actual).toBe('Hello, guest!');
});

This one doesn't pass. Let's change our code.

const sayHello = name => {
    if (!name) {
        return 'Hello, guest!';
    }

    return `Hello, ${name}!`;
}

What about an empty name?

t.it('should greet a user without a name', (t) => {
    const actual = app.sayHello('');
    t.expect(actual).toBe('Hello!');
});

One more time, the new test fails: Hello! is expected, but Hello, guest! is the actual result.

const sayHello = name => {
    if (name === '') {
        return 'Hello!';
    }

    if (!name) {
        return 'Hello, guest!';
    }

    return `Hello, ${name}!`;
}

Could we refactor this piece of code? Sure.

const sayHello = name => {
    if (name === '') {
        return 'Hello!';
    }

    return name ? `Hello, ${name}!` : 'Hello, guest!';
}

The test suite makes it possible to refactor the code easily, without being afraid of breaking something.

Step 2

The code is working great, but it appears that the requirements missed one important aspect: for some users, it's not the name which is passed, but the numeric identifier of their account. This leads to a fourth requirement:

New requirement:

  • When a numeric identifier is passed instead of the name, the code should look up for the name in the database.

Hopefully, the database access part is already developed; we just have to call db.users.findById(...).firstName. Note that since you're writing unit tests (as opposed to integration or system tests), you (1) can't use a database during your tests and (2) you can't simply rely on some code from another part of the application (one of the reasons being because regressions in their code would make your tests fail).

Therefore, you'll rely on a stub. A stub is a piece of code which substitutes the real functionality of the application by some elementary behavior which is suitable in a unit test. For instance, a stub of db.users.findById(...) for our test would be a method like that:

const findByIdStub = _ => { firstName: 'Elise' };

How do you replace the actual code by a stub? By using dependency injection. If you don't know what it is, read about SOLID principles. Robert C. Martin writes a lot about that (among many, many other subjects).

Once the dependency injection is in place, the new test can be added to the test suite:

t.it('should greet a user based on his identifier', (t) => {
    const actual = app.sayHello(123);
    t.expect(actual).toBe('Hello, Elise!');
});

This leads to the following change in the code:

const sayHello = idOrName => {
    if (idOrName === '') {
        return 'Hello!';
    }

    if (typeof idOrName === 'number') {
        const nameFromDb = this.db.users.findById(idOrName).firstName;
        return `Hello, ${nameFromDb}!`;
    }

    return idOrName ? `Hello, ${idOrName}!` : 'Hello, guest!';
}

This code becomes ways too complex and has duplicated code, don't you think? Let's refactor it one more time. In the following piece of code, only sayHello is exported, making findDisplayName private. Private methods are not tested directly by the unit tests; they are, however, tested indirectly through the public methods. You may notice that after the refactoring, all execution paths are still covered.

const findDisplayName = idOrName => {
    if (typeof idOrName === 'number') {
        return this.db.users.findById(idOrName).firstName;
    }

    return idOrName || 'guest';
};

const sayHello = idOrName => {
    if (idOrName === '') {
        return 'Hello!';
    }

    const name = this.findDisplayName(idOrName);
    return `Hello, ${name}!`;
};
  • Actually i feel like, I've read a lot of theoretical aspect and probably right now need to more practice with same sample codes to understand unit-tests better. I'll keep looking those book you offered. Thanks a lot. – Nuri Engin Dec 29 '17 at 10:59
  • 1
    @NuriEngin: sorry, it took a bit longer to write a complete example. Is it clearer now? – Arseni Mourzenko Dec 29 '17 at 11:28
  • Dear ArseniMourzenko, I've added updated unit-test snippet just end of question. So depends on your samples I've refactor assertion. Could you please review and give your opion. Only there is one thing I couldn't be sure! I've used some config/variables for both getStatusDesc() and getGlyph() so is that still a unit test assertion or integration test assertion? – Nuri Engin Dec 29 '17 at 12:19
  • As well I just added results of test-runner as comment at right-side of assertions. – Nuri Engin Dec 29 '17 at 12:26
  • 1
    @NuriEngin: it's a good start, although still very incomplete. For instance, isEmptyStr would have at least two tests, eventually more depending on the implementation. The descriptions should explain the business rules; currently, they don't. For instance for isEmptyStr, “should be false when it has string inside” can be renamed to either “should handle non-empty strings” or, if details are needed, “should return false when the string is not empty.” – Arseni Mourzenko Jan 2 '18 at 19:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.