Using tests to help us combine two objects

That is a significant problem. There are good solutions to that. Most programmers use automatic tests to ensure that nothing important gets missed out. That way you don’t risk forgetting things with manual tests.

Right now you seem to be stumbling around in the dark.
Shall I take you through the proper way to deal with things?

Yes.

The proper way is to run your own development server. That can be done on basically any personal computer using something called Node. Frequently that is paired with an online code repository such as GitHub, so that as you make successful updates to your local code, you can push those changes to that remote code repository.

The idea there is that you have multiple environments. One may be for development, another for testing, another for staging, another for production.,

As a part of that, tests are used to help ensure that the local code does what is expected of it, before pushing that code to the remote repository. There are several different ways of doing tests, such as Mocha, Jest, Jasmine, the list goes on.

The trouble though is that most of those things tend to be what you resist. However, using jsFiddle we can do some basic testing to help us develop a combinePlayerOptions function that works properly for us. The tests can remain on that jsFiddle page, and the final function can then be copied without the tests and put where you need it.

Are you ready to get started using jsFiddle?

1 Like

Yes I am ready to begin.

When we use tests to help us develop the code, it’s important that we don’t go for the gold. Instead we should circle all around it checking different things that are related to the final goal. That way we can gradually build up the different bits and pieces that are needed to achieve the final goal.

To start with, we’ll to an initial test simple test, to ensure that our testing stuff is all working properly. As Jest, arguably the most popular of the testing frameworks, requires running in Node, we’ll instead go for a testing framework that can run standalone in a browser. There are a couple of viable solutions for that, one being Jasmine, and the other being Mocha+Chai. Jasmine doesn’t seem to work well with JSFiddle, so it’s on to Mocha+Chai where Mocha is the testing framework, and Chai provides the assertions about what we expect to be

On the jsFiddle page I can search the resources for Mocha and press the symbol to use it. I also search for Mocha again so that I can use the .css version of the file too. That helps to make the screen look nice. I can also search for Chai and use that too. jsFiddle now has three resources listed, two for Mocha (js and css), and one for Chai.

In the HTML section of the page we only want a div for mocha, where the test results will be shown.

<div id="mocha"></div>

In the JS part we init mocha and chai, and tell mocha to run.

mocha.setup("bdd");
const expect = chai.expect();

mocha.run();

Above the run is where we’ll put our tests. It’s usually best to start with a simple test that true equal true, to check that all of the testing stuff is working properly.

describe("initial test", function () {
    it("checks a true case", function () {
        expect(true).to.equal(true);
    });
});

Even that simple test has helped me to catch an error. I’m told TypeError: expect is not a function because I messed up the chai part of the code. We shouldn’t invoke expect when doing that. Instead it us only the expect property that we assign instead.

// const expect = chai.expect();
const expect = chai.expect;

The test now works, and we are in a good place to remove that initial test and put in our proper first test for the combinePlayerOptions function.

Here is how jsFiddle looks with that initial test in place. https://jsfiddle.net/1zkow8x9/

How do I add it in to the code? https://jsfiddle.net/m20orbqh/

Where does it go?

With jsFiddle there is no good way to add the function into the code, other than to copy the function and paste it where you want it. Still, that means that you’ll have a separate jsFiddle page with the tests for that function.

For the first test, we just want to figure out what happens when we call combinePlayerOptions with no parameters at all. Because we intend it to combine different objects and return a combined object, it makes sense that the function should just return an empty object when none are given to it.

describe("combinePlayerOptions", function () {
    it("with no parameters, gives an object", function () {
        const combined = combinePlayerOptions();
        expect(combined).to.be.an("Object");
    });
});

The test tells us: “ReferenceError: combinePlayerOptions is not defined” which makes sense, as we don’t have that function yet. Let’s create it at the top of the code.

function combinePlayerOptions() {
}

The test now tells us: AssertionError: expected undefined to be an object which is because we aren’t returning anything yet from the function.

function combinePlayerOptions() {
    return {};
}

The tests now pass and we have the fundamentals in place for combinePlayerOptions. https://jsfiddle.net/1zkow8x9/1/

The rest of it is adding more tests and updating the function so that it passes all of the tests, which I’ll get to in the next posts.

1 Like

So far we have just one test for what happens with no function parameters. Next, is what should happen when we give is one function parameter. With two parameters things make sense because we have both a target and a source object.

With just the one parameter that’s just going to be the target. When there’s only one function parameter we should just return that parameter, as there’s nothing for it to be combined with.

    it("with one parameter, gives that same object", function() {
        const target = {
            test: "test object"
        };
        const combined = combinePlayerOptions(target);
        expect(combined.test).to.equal("test object");
    });

There is a very easy way to get that test working, and that is to check if the function was given a parameter. If it was given one, we then return an object with “test object” in it.

function combinePlayerOptions(target) {
    if (!target) {
        return {};
    }
    return {test: "test object"};
}

That is a silly solution, but it achieves one important thing which is to keep all of the tests working. Now that all of the tests are working we can refactor the code to improve it, all while still keeping all of the tests working.

Now that the tests are all passing, we can use refactoring to improve the code. With the target parameter being either undefined or an object, we can use Object.assign to add that target to an object and return the result.

function combinePlayerOptions(target) {
    if (!target) {
        return {};
    }
    return Object.assign({}, target);
}

That works, so let’s keep going. Can we remove the if statement, demonstrating that the assign helps us to deal with an undefined target parameter too?

function combinePlayerOptions(target) {
    return Object.assign({}, target);
}

Yes we can. We have done the full testing cycle of: 1. a failing test, 2. passing the test, 3. refactoring while passing. https://jsfiddle.net/1zkow8x9/1/

The next part with multiple function parameters is where things get tricky, because there are several different types of situations to deal with. Fortunately we can deal with them one at a time using our tests, and ensure that the combinePlayerOptions function can properly deal with all of them.

1 Like

The next part is about when we have two objects as parameters. One called target, and the other called source. Those are just default names for them. I could instead call them defaultOptions and playerOptions, and might still do that later on. The whole intention of using target and source is to indicate that we want the information in the source object to overwrite the information in the target object.

With the target parameter, we could have source parameter add itself to the target object, or we could have a source parameter overwrite a parameter that is in the target object. Additionally, if that target parameter is playerVars we don’t want playerVars to be completely replaced with the playerVars from the source object. We instead want the two to be merged together.

So first off, a simple test is checking that a parameter from a source object gets added to the target object.

    it("with two parameters, source params are copied too", function() {
        const target = {};
        const source = {
            test: "test value"
        };
        const combined = combinePlayerOptions(target, source);
        expect(combined.test).to.equal("test value");
    });

The test error tells us: AssertionError: expected undefined to equal 'test value'

Let’s add a source parameter to the combinePlayerOptions function and get things passing in a simple way. How we do that is we check for what was true for all of the previous tests, and after that we do something simple to get the test passing.

function combinePlayerOptions(target, source) {
    if (!source) {
        return Object.assign({}, target);
    }
    return {test: "test value"};
}

Once again that is a silly solution, but the tests all pass. We can now refactor the code to improve it. Here, I would like to try assigning both target and source.

function combinePlayerOptions(target, source) {
    if (!source) {
        return Object.assign({}, target);
    }
    return Object.assign({}, target, source);
}

Good, that works Can we remove the if statement now?

function combinePlayerOptions(target, source) {
    return Object.assign({}, target, source);
}

Yes we can, and the tests all still pass. https://jsfiddle.net/1zkow8x9/3/

The next test is about what happens when target has a property, and source has the same property name too. The source property should overwrite the value that was on the target property.

    it("with two parameters, source params overwrite target params", function() {
        const target = {
            test: "target value"
        };
        const source = {
            test: "source value"
        };
        const combined = combinePlayerOptions(target, source);
        expect(combined.test).to.equal("source value");
    });

That test immediately passes, so no change needs to happen to the function. https://jsfiddle.net/1zkow8x9/4/

Next up is to test what happens when it also contains a playerVars object. Things will get messier there.

1 Like

Because the playerVars has several different situations happening with it, we’ll put those tests into their own describe section. The describe sections are mostly for our own benefit, to help us recognise that there are similar tests grouped together.

    describe("playerVars", function () {
    
    });

There are four different situations when it comes to playerVars, that can be represented in the following table:

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |    [?]      |    [?]    |    [?]
no params   |    [?]      |    [?]    |    [?]
has params  |    [?]      |    [?]    |    [?]

We need to check that all four of those interactions properly work. The hardest of them is when both have parameters, so we’ll do that one last.

Checking that things work when they both have no properties has already been done with the existing tests, so we can mark that one as being done.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |           |
no params   |             |           |
has params  |             |           |

Let’s get the rest of the no property situations tested.

    describe("playerVars", function () {
        it("with empty source playerVars, includes playerVars", function() {
            const target = {};
            const source = {
                playerVars: {}
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars).to.be.an("object");
        });
    });

That test passes, so we can add a checkbox to the list:

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |           |
no params   |     ✅     |           |
has params  |             |           |

And when the source playerVars has parameters, those need to exist too in the returned object.

        it("with source playerVars property, includes playerVars property", function() {
            const target = {};
            const source = {
                playerVars: {test: "test value"}
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("test value");
        });

That works too, so we can add another checkbox.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |           |
no params   |     ✅     |           |
has params  |     ✅     |           |

We now have reliable tests for all of those situations, which will help to catch any failure as we update the function. https://jsfiddle.net/1zkow8x9/5/

The next tests will be for when the target object has an empty playerVars object, after which the final part will be the most interesting of all.

1 Like

The next tests are about when the target object also has an empty playerVars object.

When the target playerVars is empty and the source object has no playerVars property, the target playerVars should remain empty.

        it("with no source playerVars, keeps an empty target playerVars", function() {
            const target = {
                playerVars: {}
            };
            const source = {};
            const combined = combinePlayerOptions(target, source);
            expect(Object.values(combined.playerVars).length).to.equal(0);
        });

That test works well, so we can add another checkbox to the list.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |
no params   |     ✅     |           |
has params  |     ✅     |           |

Adding checkboxes is nice, as it gives a real sense of progress.

The next test is for when the target and the source both have empty playerVars.

        it("with both having empty playerVars, keeps it empty", function() {
            const target = {
                playerVars: {}
            };
            const source = {
                playerVars: {}
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars).to.be.an("object");
            expect(Object.values(combined.playerVars).length).to.equal(0);
        });

That one also passes so we add another checkbox:

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |
no params   |     ✅     |     ✅    |
has params  |     ✅     |           |

And now we check that the playerVars gets updated when the target playerVars is empty and the source playerVars has something in it.

        it("with source playerVars params, updates when target playerVars is empty", function() {
            const target = {
                playerVars: {}
            };
            const source = {
                playerVars: {
                    test: "source value"
                }
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("source value");
        });

That all works, so update the table.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |
no params   |     ✅     |     ✅    |
has params  |     ✅     |     ✅    |

I’m still waiting for a problem to occur, but that’s likely to occur in the last section when the target playerVars has parameters already in it. We’ll get to that next.

1 Like

The next test is for when target playerVars has a parameter and source has no playerVars.

        it("with target playerVars param and no source playerVars, keeps playerVars param", function() {
            const target = {
                playerVars: {
                    test: "target value"
                }
            };
            const source = {};
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("target value");
        });

That all works, so another checkbox updates the table.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |     ✅
no params   |     ✅     |     ✅    |
has params  |     ✅     |     ✅    |

I’m getting nervous now because there are only two situations left to check.
Well no, there’s more. There’s also the situation when playerVars is undefined which would result in a 16x16 grid. That’s not expected use for the function, but I might check what happens there afterwards.

For now, we are checking what happens when target has a playerVars param and source has an empty playerVars param. From experience, this is what has caused trouble for you in your code.

        it("with target playerVars param and empty source playerVars, keeps playerVars param", function() {
            const target = {
                playerVars: {
                    test: "target value"
                }
            };
            const source = {
                playerVars: {}
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("target value");
        });

And we at last get a failing test.
AssertionError: expected undefined to equal 'target value'

The source playerVars is clobbering everything that was in the target playerVars.

We can deal with that by assigning the assigned object to a combined variable. That way we can separately assign the playerVars objects.

function combinePlayerOptions(target, source) {
    const combined = Object.assign({}, target, source);
    combined.playerVars = Object.assign({}, target.playerVars, source.playerVars);
    return combined;
}

But, some of the tests still fail telling us:
TypeError: Cannot read properties of undefined (reading 'playerVars')

That is because target or source are undefined. We can add a default parameter value to them, to solve that problem with those tests.

function combinePlayerOptions(target = {}, source = {}) {

and all of the tests pass. We can add another checkbox to the table now.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |     ✅
no params   |     ✅     |     ✅    |     ✅
has params  |     ✅     |     ✅    |

There’s one last situation to check, and that’s when both target and source playerVars have parameters. When they’re the same parameters source should overwrite target.

        it("with the same target and source playerVars params, source should win", function() {
            const target = {
                playerVars: {
                    test: "target value"
                }
            };
            const source = {
                playerVars: {
                    test: "source value"
                }
            }
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("source value");
        });

That one works, and the last test that we plan to do is when both target and source have different playerVars parameters. Both of those parameters should be kept.

        it("with the different target and source playerVars params, both are kept", function() {
            const target = {
                playerVars: {
                    targetTest: "target value"
                }
            };
            const source = {
                playerVars: {
                    sourceTest: "source value"
                }
            }
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.targetTest).to.equal("target value");
            expect(combined.playerVars.sourceTest).to.equal("source value");
        });

That test passes fine, and so we can add a final checkbox to the table.

     target | no property | no params | has params
source      | ----------- | --------- | ----------
no property |     ✅     |     ✅    |     ✅
no params   |     ✅     |     ✅    |     ✅
has params  |     ✅     |     ✅    |     ✅

That’s a lot of tests, and the function is very small, being only the following:

function combinePlayerOptions(target = {}, source = {}) {
    const combined = Object.assign({}, target, source);
    combined.playerVars = Object.assign({}, target.playerVars, source.playerVars);
    return combined;
}

But as the tests all attest, that function is capable of correctly dealing with a wide variety of situations. https://jsfiddle.net/1zkow8x9/6/

We also need to check that other expected objects work properly such as with the events object. We could even make the code more generalized so that it works with all nested objects instead of playerVars and events, but that will be for the next post.

1 Like

When looking at the playerVars test results, I realized that they were a bit tricky to understand. As a result I renamed the tests so that they are easier to follow.

with no target and no source playerVars, doesn't have playerVars‣
with no target and empty source playerVars, includes playerVars‣
with no target and source playerVars property, includes playerVars property‣
with empty target playerVars and no source playerVars, keeps playerVars‣
with empty target and source playerVars, keeps it empty‣
with empty target playerVars and source playerVars params, updates playerVars‣
with target playerVars param and no source playerVars, keeps playerVars param‣
with target playerVars param and empty source playerVars, keeps playerVars param‣
with target and source playerVars params the same, source should win‣
with target and source playerVars params different, both are kept

That way it’s much easier to tell that the tests are grouped by the types of target. https://jsfiddle.net/1zkow8x9/7/

The other situation that I wanted to check is that a target events object does not get clobbered by the source events object.

    describe("events", function () {
    	it("doesn't clobber the target events object", function () {
            const target = {
                events: {
                    onPlayerReady: "onPlayerReady function"
                }
            };
            const source = {
                events: {
                    onStateChange: "another function"
                }
            }
            const combined = combinePlayerOptions(target, source);
            expect(combined.events.onPlayerReady).to.equal("onPlayerReady function");
            expect(combined.events.onStateChange).to.not.equal("onStateChange function");
        });
    });

That test currently fails, because the target events object is getting clobbered by the source one.

We can fix that by adding another line to assign the events object.

function combinePlayerOptions(target = {}, source = {}) {
    const combined = Object.assign({}, target, source);
    combined.events = Object.assign({}, target.events, source.events);
    combined.playerVars = Object.assign({}, target.playerVars, source.playerVars);
    return combined;
}

We don’t know if there will be other objects such as events and playerVars that also need to be dealt with. Instead of doing them individually, we can instead search through all of the object properties and do that same thing when we find that the events property is an object, or when the playerVars property is an object.

I can try to deal with that by moving events into a loop.

function combinePlayerOptions(target = {}, source = {}) {
    const combined = Object.assign({}, target, source);
    Object.keys(target).forEach(function (prop) {
        if (prop === "events") {
            combined.events = Object.assign({}, target.events, source.events);
        }
    });
    combined.playerVars = Object.assign({}, target.playerVars, source.playerVars);
    return combined;
}

And then to remove events from inside of the forEach loop:

function combinePlayerOptions(target = {}, source = {}) {
    const combined = Object.assign({}, target, source);
    Object.keys(target).forEach(function (prop) {
        if (typeof target[prop] === "object") {
            combined[prop] = Object.assign({}, target[prop], source[prop]);
        }
    });
    combined.playerVars = Object.assign({}, target.playerVars, source.playerVars);
    return combined;
}

And finally to remove that playerVars line from inside of the function too, as that should also be nicely dealt with by the forEach function.

function combinePlayerOptions(target = {}, source = {}) {
    const combined = Object.assign({}, target, source);
    Object.keys(target).forEach(function (prop) {
        if (typeof target[prop] === "object") {
            combined[prop] = Object.assign({}, target[prop], source[prop]);
        }
    });
    return combined;
}

All of the tests still pass. That’s amazing!

We now have a combinePlayerOptions function that can deal with any situation that we would normally come across. https://jsfiddle.net/1zkow8x9/8/

The code still doesn’t deal with multiply nested objects or with recursive objects, or combining arrays, or a few other weird situations like that, but that’s what external libraries to merge objects are especially designed for. The function is enough for our purposes.

To finish off in the next post I will:

  • rename target and source to be playerOptions
  • get JSLint to be happy with the code
  • and see what happens when we include an undefined playerVars into that table of tests
1 Like

It can be tricky to understand what the combinePlayerOptions function does when it has target and source parameter names, so let’s rename them. Calling them playerOptions1 and playerOptions2 immediately tells us what we expect the function to do.

function combinePlayerOptions(playerOptions1 = {}, playerOptions2 = {}) {
    ...
}

JSLint had a few complaints, but mostly they were about long test description lines. Those have all been tweaked and JSLint is now happy.

The last thing to investigate is what happens when the playerVars parameter is undefined, so let’s add that to the table.

     target | no property | undefined | no params | has params
source      | ----------- | --------- | --------- | ----------
no property |     ✅     |           |     ✅    |     ✅
undefined   |             |           |           |
no params   |     ✅     |           |     ✅    |     ✅
has params  |     ✅     |           |     ✅    |     ✅

Let’s find out what happens by testing when source playerVars is undefined.

        it("with no target and undefined source playerVars", function () {
            const target = {};
            const source = {
                playerVars: undefined
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars).to.equal(undefined);
        });

Well that one works:

     target | no property | undefined | no params | has params
source      | ----------- | --------- | --------- | ----------
no property |     ✅     |           |     ✅    |     ✅
undefined   |     ✅     |           |           |
no params   |     ✅     |           |     ✅    |     ✅
has params  |     ✅     |           |     ✅    |     ✅

Let’s see what happens with the others when source is undefined.

        it("with undefined target source playerVars", function () {
            const target = {
                playerVars: undefined
            };
            const source = {
                playerVars: undefined
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars).to.equal(undefined);
        });
...
        it("with empty target and undefined source playerVars", function () {
            const target = {
                playerVars: {}
            };
            const source = {
                playerVars: undefined
            };
            const combined = combinePlayerOptions(target, source);
            expect(Object.values(combined.playerVars).length).to.equal(0);
        });
...
        it("with target playerVars and undefined source playerVars", function () {
            const target = {
                playerVars: {
                    test: "target value"
                }
            };
            const source = {
                playerVars: undefined
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("target value");
        });

Well, those all work. Let’s update the table.

     target | no property | undefined | no params | has params
source      | ----------- | --------- | --------- | ----------
no property |     ✅     |           |     ✅    |     ✅
undefined   |     ✅     |     ✅    |    ✅    |     ✅
no params   |     ✅     |           |     ✅    |     ✅
has params  |     ✅     |           |     ✅    |     ✅

That’s a full undefined row, let’s now see what happens when we complete tests for the undefined columns too.

        it("with undefined target and empty source", function () {
            const target = {
                playerVars: undefined
            };
            const source = {};
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars).to.equal(undefined);
        });
...
        it("with undefined target and empty source", function () {
            const target = {
                playerVars: undefined
            };
            const source = {
                playerVars: {}
            };
            const combined = combinePlayerOptions(target, source);
            expect(Object.values(combined.playerVars).length).to.equal(0);
        });
...
        it("with undefined target and source playerVars prop", function () {
            const target = {
                playerVars: undefined
            };
            const source = {
                playerVars: {
                    test: "source value"
                }
            };
            const combined = combinePlayerOptions(target, source);
            expect(combined.playerVars.test).to.equal("source value");
        });

Well colour me surprised - all of those tests work too. Let’s update the table.

     target | no property | undefined | no params | has params
source      | ----------- | --------- | --------- | ----------
no property |     ✅     |     ✅    |    ✅    |     ✅
undefined   |     ✅     |     ✅    |    ✅    |     ✅
no params   |     ✅     |     ✅    |    ✅    |     ✅
has params  |     ✅     |     ✅    |    ✅    |     ✅

We now have confirmation that the combinePlayerOptions function really is capable of correctly handling all of those situations. https://jsfiddle.net/1zkow8x9/10/

In the next post I’ll finish up by going through how we’ll include that combinePlayerOptions into your existing code.

1 Like

5 posts were split to a new topic: Using combinePlayerOptions into existing code

10 posts were split to a new topic: Troubleshooting playerVars on embedded youtube