3

So I want to test if my breadcrumb child component shows expected breadcrumb when I go to a certain url,

for instance when I go to say

 http:// <domain>?c__view=contractDetail&c__id=8000M000002IWUMQA4

Then my breadcrumbs shows this in the UI: enter image description here

And I want to test the above behaviour

Now the trouble I have is the breadcrumd is child component which is mocked out in the test , so it doesn't really render much , this is what it renders in the snapshot

    <runtime_online_sales-breadcrumb>
      #shadow-root(open)
        breadcrumb
    </runtime_online_sales-breadcrumb>

I don't want to use the actual child component in test, so I decided I could test the model property rendering the UI instead, but the model property is a @track and not accesible in the test i.e.:

@track _breadcrumbs;

and set as so:

this._breadcrumbs = getBreadcrumbs({
                view: this.currentView,
                recName,
                recId: this.urlParamId,
            });

I understand LWC wants use to test the UI then strict unit test the functions, but want to know what should I do in this situation , is it alright to make _breadcrumbs @api just to open it to testing?

8
  • 1
    What are you passing to the child component - any existing api fields? A component should only test what it's in control of or doing (ex. passing right values to child). If you want to test/assert the child component renders as expected in certain conditions - that would be done in a child component jest test.. Mar 21 at 18:00
  • I pass _breadcrumbs along with three other properties to child component, but I can't access _breadcrumbs from the test to test it unless I make it @api Mar 21 at 18:10
  • 1
    I've done that including a comment to say it is for testing purposes only. Testing "logic that can break" directly makes more sense than testing its UI side effects. And generally IMHO, purist attitudes to test automation result in excessive and obscure code. Maybe one day there will be an @TestVisible annotation in LWC too? But you may have to evade your local "best practice" police.
    – Keith C
    Mar 21 at 18:45
  • Personally I believe you should consider the component as a black box, only testing its inputs and outputs. If you have behaviours you want to test that are not easily accessible, I personally would consider turning this behaviour into a headless LWC that exposes the required behaviour as functions that are used by your UI component but are then also directly testable in themselves.
    – Phil W
    Mar 21 at 19:40
  • 1
    Oh, and the child mock focuses on the API so the actual internals of the child are irrelevant.
    – Phil W
    Mar 22 at 8:11

3 Answers 3

6

In my opinion, as a long-time developer, I believe you should consider the component as a black box, only testing its inputs and outputs.

If you have behaviours you want to test that are not easily accessible, I personally would consider turning this behaviour into a headless LWC that exposes the required behaviour as functions that are used by your UI component but are then also directly testable in themselves.

For me, exposing internals of a component just for testing is wrong, though component decomposition is not.

Exposing internals is likely to lead to inappropriate use at some later time when a different developer comes along and misunderstands how the component is intended to work.

EDIT:

Following on from a comment by @KeithC, and to reinforce my first point, all LWC components should be treated as a black box, with the unit test relying on its API only. An LWC's API consists of:

  1. The component's name.
  2. Its @api properties.
  3. The (custom) events it emits.

In line with true unit testing (compared with a form of "integration testing"), the one internal aspect that the unit test should care about is other components that are used by the component under test (the "parent" component).

The goal of the component's unit tests is to test the component's behaviour in isolation of the behaviour of any so-called "child" components.

Thus these "child" components should be mocked with behaviours that permit the "parent" to be properly exercised. Such mocks should only consider the "child" component's API (so those three points above) and not the child's implementation - just behavioural expectations. When wanting to verify that the "parent" uses the child's API appropriately, add necessary asserts in the mocked child component.

The actual "child" components will, in turn, have their own unit tests that test them in isolation too.

2
  • yes, I agree having an @api tag can be misleading , I already have test for the child component itself. in this case I do think there is value to test that the parent sends right inputs to it's child. the one way I see this working is maintaining some state/functionallity in child stub and test that. Also as someone mentioned '@openTOTestClass' decorator doesn't sound like a bad idea Mar 21 at 22:42
  • 1
    Instead of having @TestVisible in Apex and introducing an equivalent in LWC I would much prefer full reflection capabilities. That way a test could use reflection to access "private parts". That said, I personally disagree with @TestVisible since this is appearing in production code and is, for me, untidy, and is opening up the "black box" of the code under test. We structure our code to avoid the need to do this 99.5% of the time, essentially using dependency injection to allow mocking of "child" code.
    – Phil W
    Mar 22 at 8:51
4

Remember that @api properties are read-only, so adding that will give you lint errors (and possibly erratic behavior later). You might consider listening for side effects, if any, such as a navigation event that should fire when the breadcrumbs change. Also, remember you can check the shadowRoot element to make sure the DOM renders as you expect, which is what you'd normally do when testing that a UI is updating.

0

So I ended up using prototype and mocking all doing a 'strict' unit test.

This is what I mean:

UI:

<parent>
<child-breadcrumb breadcrumbs={_breadcrumbs}/>
<parent>

parent.js:

/// lwc imports..
export default class parent extends NavigationMixin(LightningElement){
  // navigation code...
     contracts = someApiCallreturningContracts();
     currentView = setOnCurrentPageRefernceUsingNavigationMix;


     /**
     * Builds bread crumb using the c__id in URL, errors out using a modal if existing contract don't content a contract
     * with the urlParamId
     */
    buildBreadCrumbForContractDetail() {
        const selectedContract = this.getContractFromUrlId();
        if (!selectedContract) {
            this.showSelectedContractNotFoundError();
        } else {
            this._breadcrumbs = getBreadcrumbs({
                view: this.currentView,
                recName: selectedContract.contractNumber,
                recId: this.urlParamId,
            });
        }
    }
  
}

parent.test.js

getBreadcrumbs = // some mock or stub function

describe('.buildBreadCrumbForContractDetail', () => {
    let scope;
    beforeEach(() => {
        scope = {
            currentView: VIEW.CONTRACT_DETAIL,
            contracts: [],
            [NavigationMixin.Navigate]: jest.fn(),
            getContractFromUrlId: jest.fn(), //gotta test them separately
            showSelectedContractNotFoundError: jest.fn(), //gotta test them separately
        };
    });

    it('should call showSelectedContractNotFoundError on no contract', () => {
        scope.getContractFromUrlId.mockReturnValue(null);
        OnlineSalesHomePage.prototype.buildBreadCrumbForContractDetail.call(scope);
        expect(scope.showSelectedContractNotFoundError).toHaveBeenCalled();
    });

    it('should set breadcrumbs if a contract is selected', () => {
        const expectedContract = {
            contractId: 1
        };
        Object.assign(scope, {
            getBreadcrumbs,
            urlParamId: 1
        });
        scope.getContractFromUrlId.mockReturnValue(expectedContract);
        Parent.prototype.buildBreadCrumbForContractDetail.call(scope);
        const expectedLastBreadCrumbSegment = {
            name: 'contractDetail',
            id: 'contractDetail',
            label: undefined,
            urlNav: true,
            recId: expectedContract.contractId
        };
        expect(scope._breadcrumbs).toHaveLength(3);
        expect(scope._breadcrumbs[2]).toEqual(expectedLastBreadCrumbSegment);
    });
});

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.