title: Angular Unit Test including FileReader.onload doesn't work with async/whenStable nor fakeAsync/tick
body: <p>I'm adding some unit tests to my angular 7 app but I'm not sure how to handle the interaction with a FileReader since using async/whenStable, fakeAsync and promises don't work as expected.</p>
  
  <ul>
  <li><strong>The behavior I'm testing</strong>: I need to verify the service <code>documentService.sendDocument</code> is called once a file is uploaded.    </li>
  </ul>
  
  <h2><strong>1st Approach</strong>:</h2>
  
  <p>Before I found issues testing FileReader.onload I just tried to test it without async.</p>
  
  <p><strong>Controller Code</strong></p>
  
  <pre><code>onFileGovernmentIdChange(event) {
      console.log('::: Init onFileGovernmentIdChange');
      const updateFunction = () =&gt; {
          console.log('::: BEFORE update fileGovernmentIdUploaded - ', this.fileGovernmentIdUploaded);
          this.fileGovernmentIdUploaded = true;
          console.log('::: AFTER update fileGovernmentIdUploaded - ', this.fileGovernmentIdUploaded);
      }
      this.saveFileFromInputSimple(event, DOCUMENT_TYPE.STORE_GOVERNMENT_ID, updateFunction);
      console.log('::: End onFileGovernmentIdChange');
  }
  
  private saveFileFromInputSimple(event, documentType: DOCUMENT_TYPE, updateState: () =&gt; any) {
      console.log('::: Init saveFileFromInputSimple');
      const reader = new FileReader();
  
      if (event.target.files &amp;&amp; event.target.files.length &gt; 0) {
          console.log('::: Event with files...');
          const file = event.target.files[0];
          reader.onload = () =&gt; {
              this.documentService
                  .sendDocument(file.name, file.type, reader.result.toString(), documentType)
                  .subscribe(
                      response =&gt; {
                          console.log('::: sendDocument - Subscribe OK');
                          updateState()
                      },
                      error =&gt; {
                          console.log('::: sendDocument - Subscribe ERROR');
                          this.showDefaultErrorDialog()
                      }
                  );
          };
          console.log('::: Onload callback assigned...');
          reader.readAsDataURL(file);
      }
      console.log('::: End saveFileFromInputSimple');
  }
  
  
  </code></pre>
  
  <p><strong>UnitTest spec code</strong></p>
  
  <pre><code>fit('simple testing', () =&gt; {
      const documentService = TestBed.get(DocumentsService);
      const catalogService: CatalogService = TestBed.get(CatalogService);
      const customEvent = {
          target: {
              files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
          }
      };
  
      const commerceResponse = new CommerceResponse();
      commerceResponse.commissionPercentage = '11';
  
      spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
      spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
      spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
  
      fixture.detectChanges();//Apply onInit changes
      console.log('::: Before calling onFileGovernmentIdChange()');
      component.onFileGovernmentIdChange(customEvent);
      console.log('::: After calling onFileGovernmentIdChange()');
      console.log('::: Before expects... ');
      expect(component.fileGovernmentIdUploaded).toBeTruthy();
      // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
      console.log('::: After expects... ');
  });
  </code></pre>
  
  <p><strong>UnitTest result</strong></p>
  
  <pre><code>LOG: '::: Before calling onFileGovernmentIdChange()'
  LOG: '::: Init onFileGovernmentIdChange'
  LOG: '::: Init saveFileFromInputSimple'
  LOG: '::: Event with files...'
  LOG: '::: Onload callback assigned...'
  LOG: '::: End saveFileFromInputSimple'
  LOG: '::: End onFileGovernmentIdChange'
  LOG: '::: After calling onFileGovernmentIdChange()'
  LOG: '::: Before expects... '
  LOG: '::: After expects... '
  Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
          Expected undefined to be truthy.
              at UserContext.&lt;anonymous&gt; src/app/documents/documents.component.spec.ts:146:52)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
              at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:387:1)
  </code></pre>
  
  <p><strong>UnitTest analysis</strong></p>
  
  <p>The code inside the <code>onload</code> method is never executed</p>
  
  <h2><strong>2nd Approach</strong></h2>
  
  <p>Testing FileReader.onload without Observers: Just to follow the workflow I removed the Observer inside the <code>onload</code> to verify if that could be the problem.</p>
  
  <p><strong>Controller Code</strong></p>
  
  <pre><code>private saveFileFromInputSimple(event, documentType: DOCUMENT_TYPE, updateState: () =&gt; any) {
      console.log('::: Init saveFileFromInputSimple');
      const reader = new FileReader();
  
      if (event.target.files &amp;&amp; event.target.files.length &gt; 0) {
          console.log('::: Event with files...');
          const file = event.target.files[0];
          reader.onload = () =&gt; {
              console.log('::: ONLOAD executed');
          };
          console.log('::: Onload callback assigned...');
          reader.readAsDataURL(file);
      }
      console.log('::: End saveFileFromInputSimple');
  }
  </code></pre>
  
  <p><strong>UnitTest spec code</strong></p>
  
  <p>Same as 1st approach</p>
  
  <p><strong>UnitTest result</strong></p>
  
  <pre><code>LOG: '::: Before calling onFileGovernmentIdChange()'
  LOG: '::: Init onFileGovernmentIdChange'
  LOG: '::: Init saveFileFromInputSimple'
  LOG: '::: Event with files...'
  LOG: '::: Onload callback assigned...'
  LOG: '::: End saveFileFromInputSimple'
  LOG: '::: End onFileGovernmentIdChange'
  LOG: '::: After calling onFileGovernmentIdChange()'
  LOG: '::: Before expects... '
  LOG: '::: After expects... '
  Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
          Expected undefined to be truthy.
              at UserContext.&lt;anonymous&gt; src/app/documents/documents.component.spec.ts:146:52)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
              at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:387:1)
  Chrome 71.0.3578 (Mac OS X 10.13.6): Executed 1 of 46 (1 FAILED) (0 secs / 0.314 secs)
  Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
          Expected undefined to be truthy.
              at UserContext.&lt;anonymous&gt; src/app/documents/documents.component.spec.ts:146:52)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
              at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
  LOG: '::: ONLOAD executed'
  </code></pre>
  
  <p><strong>UnitTest analysis</strong></p>
  
  <p>Now the code in the <code>onload</code> method is executed but looks like an async operation because it was executed at the end.</p>
  
  <h2><strong>3rd Approach</strong></h2>
  
  <p>Testing FileReader.onload with async unit test: As I found some kind of async operation I included the <code>async/fixture.whenStable</code> angular combination to wait for async code completion.</p>
  
  <p><strong>Controller Code</strong></p>
  
  <p>Same as 2nd approach</p>
  
  <p><strong>UnitTest spec code</strong></p>
  
  <pre><code>fit('Testing with async/fixture.whenStable', async(() =&gt; {
      const documentService = TestBed.get(DocumentsService);
      const catalogService: CatalogService = TestBed.get(CatalogService);
      const customEvent = {
          target: {
              files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
          }
      };
  
      const commerceResponse = new CommerceResponse();
      commerceResponse.commissionPercentage = '11';
  
      spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
      spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
      spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
  
      fixture.detectChanges();//Apply onInit changes
      console.log('::: Before calling onFileGovernmentIdChange()');
      component.onFileGovernmentIdChange(customEvent);
      console.log('::: After calling onFileGovernmentIdChange()');
      console.log('::: Before expects... ');
  
      fixture.whenStable().then(() =&gt; {
          fixture.detectChanges();
          console.log('::: whenStable Init');
          expect(component.fileGovernmentIdUploaded).toBeTruthy();
          // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
          console.log('::: whenStable End');
      });
  
      console.log('::: After expects... ');
  }));
  </code></pre>
  
  <p><strong>UnitTest result</strong></p>
  
  <pre><code>LOG: '::: Before calling onFileGovernmentIdChange()'
  LOG: '::: Init onFileGovernmentIdChange'
  LOG: '::: Init saveFileFromInputSimple'
  LOG: '::: Event with files...'
  LOG: '::: Onload callback assigned...'
  LOG: '::: End saveFileFromInputSimple'
  LOG: '::: End onFileGovernmentIdChange'
  LOG: '::: After calling onFileGovernmentIdChange()'
  LOG: '::: Before expects... '
  LOG: '::: After expects... '
  LOG: '::: whenStable Init'
  LOG: '::: whenStable End'
  **LOG: '::: ONLOAD executed'**
  </code></pre>
  
  <p><strong>UnitTest analysis</strong></p>
  
  <p>As expected, the whenStable code is executed when the method has finished, however, the <code>onload</code> method continues executing at the end.
  Googling I found that could be better to wrap the onload part in a Promise to be sure it's tracked by Angular async.</p>
  
  <h2><strong>4th Approach</strong>:</h2>
  
  <p>Wrap the <code>onload</code> in a promise and verify it works with the <code>async/whenStable</code> structure.</p>
  
  <p><strong>Controller Code</strong></p>
  
  <pre><code>private readFileAsync(file): Promise&lt;string&gt; {
      console.log('::: readFileAsync Init');
      return new Promise((resolve, reject) =&gt; {
          const reader = new FileReader();
          reader.onload = () =&gt; {
              console.log(':::: Promise Resolved in ONLOAD')
              resolve(reader.result.toString());
          }
          reader.onerror = reject;
          reader.readAsDataURL(file);
      });
  }
  
  private saveFileFromInputWithPromise(event, documentType: DOCUMENT_TYPE, updateState: () =&gt; any) {
      console.log('::: Init saveFileFromInputWithPromise');
  
      if (event.target.files &amp;&amp; event.target.files.length &gt; 0) {
          console.log('::: Event with files...');
          const file = event.target.files[0];
  
          this.readFileAsync(file)
              .then(fileContent =&gt; {
                  console.log('::: File with content', fileContent);
                  updateState();
              }).catch(error =&gt; console.log('::: File load error', error));
      }
      console.log('::: End saveFileFromInputWithPromise');
  }
  </code></pre>
  
  <p><strong>UnitTest spec code</strong></p>
  
  <p>Same as 3rd approach</p>
  
  <p><strong>UnitTest result</strong></p>
  
  <pre><code>LOG: '::: Before calling onFileGovernmentIdChange()'
  LOG: '::: Init onFileGovernmentIdChange'
  LOG: '::: Init saveFileFromInputWithPromise'
  LOG: '::: Event with files...'
  LOG: '::: readFileAsync Init'
  LOG: '::: End saveFileFromInputWithPromise'
  LOG: '::: End onFileGovernmentIdChange'
  LOG: '::: After calling onFileGovernmentIdChange()'
  LOG: '::: Before expects... '
  LOG: '::: After expects... '
  LOG: '::: whenStable Init'
  LOG: '::: whenStable End'
  LOG: ':::: Promise Resolved in ONLOAD'
  LOG: '::: File with content', 'data:pdf;base64,c3NkZnNkZ2RqZ2hkc2xramdoZGpn'
  LOG: '::: BEFORE update fileGovernmentIdUploaded - ', undefined
  LOG: '::: AFTER update fileGovernmentIdUploaded - ', true
  Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent Testing with async/fixture.whenStable FAILED
          Expected undefined to be truthy.
              at http://localhost:9877/_karma_webpack_/webpack:/src/app/documents/documents.component.spec.ts:176:56
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
              at AsyncTestZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.AsyncTestZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:713:1)
              at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:285:1)
  
  </code></pre>
  
  <p><strong>UnitTest analysis</strong></p>
  
  <p>Again the Promise is resolved after the <code>whenStable</code> function.</p>
  
  <h2><strong>5th Approach</strong>:</h2>
  
  <p>As the <code>async/whenStable</code> didn't work as expected I tried changing to <code>fakeAsync/tick/flush</code> structure.</p>
  
  <p><strong>Controller Code</strong></p>
  
  <p>Same as 4th Approach (Including <code>onload</code> in a promise)</p>
  
  <p><strong>UnitTest spec code</strong></p>
  
  <pre><code>fit('Testing with fakeAsync/tick/flush', fakeAsync(() =&gt; {
      const documentService = TestBed.get(DocumentsService);
      const catalogService: CatalogService = TestBed.get(CatalogService);
      const customEvent = {
          target: {
              files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
          }
      };
  
      const commerceResponse = new CommerceResponse();
      commerceResponse.commissionPercentage = '11';
  
      spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
      spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
      spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
  
      fixture.detectChanges();
      console.log('::: Before calling onFileGovernmentIdChange()');
      component.onFileGovernmentIdChange(customEvent);
      console.log('::: After calling onFileGovernmentIdChange()');
      console.log('::: Before expects... ');
  
      fixture.detectChanges();
      tick();
      flushMicrotasks();
      flush();
      console.log('::: After Flush Init');
      expect(component.fileGovernmentIdUploaded).toBeTruthy();
      // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
      console.log('::: After Flush End');
      console.log('::: After expects... ');
  }));
  </code></pre>
  
  <p><strong>UnitTest result</strong></p>
  
  <pre><code>LOG: '::: Before calling onFileGovernmentIdChange()
  LOG: '::: Init onFileGovernmentIdChange'
  LOG: '::: Init saveFileFromInputWithPromise'
  LOG: '::: Event with files...'
  LOG: '::: readFileAsync Init'
  LOG: '::: End saveFileFromInputWithPromise'
  LOG: '::: End onFileGovernmentIdChange'
  LOG: '::: After calling onFileGovernmentIdChange()'
  LOG: '::: Before expects... '
  LOG: '::: After Flush Init'
  LOG: '::: After Flush End'
  LOG: '::: After expects... '
  Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent Testing with async/fixture.whenStable FAILED
          Expected undefined to be truthy.
              at UserContext.&lt;anonymous&gt; src/app/documents/documents.component.spec.ts:211:52)
              at UserContext.&lt;anonymous&gt; node_modules/zone.js/dist/zone-testing.js:1424:1)
              at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
              at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
  LOG: ':::: Promise Resolved in ONLOAD'
  LOG: '::: File with content', 'data:pdf;base64,c3NkZnNkZ2RqZ2hkc2xramdoZGpn'
  LOG: '::: BEFORE update fileGovernmentIdUploaded - ', undefined
  LOG: '::: AFTER update fileGovernmentIdUploaded - ', true
  </code></pre>
  
  <p><strong>UnitTest analysis</strong></p>
  
  <p>Again, the Promise is resolved after the fakeAsync/tick/flushMicrotasks/flush structure</p>
  
  <h3>What is wrong?</h3>
  
  <p>I followed every tutorial I found and every different approach trying to include the <code>FileReader.onload</code> in my test (as that method call the service I want to spy and verify) but always the method resolve after the async blocks provided by Angular. I saw other approaches where the <code>window.fileReader</code> is mocked but that's not the purpose of my test.</p>
  
  <p>So could anybody tell me what is wrong in my code or in the way I'm testing?</p>