javascript - Enhetstestning av NodeJS-ström med jest - timeout-problem


Givet följande NodeJS Transform Stream:

class ObjectToCSVTransform extends Transform {
  private hasSetHeaders: boolean;

  constructor() {
    super({ objectMode: true });
    this.hasSetHeaders = false;
  }

  static create(): ObjectToCSVTransform {
    return new this();
  }

  _write(
    object: Record<string, unkwnown>,
    _encoding: BufferEncoding,
    callback: TransformCallback
  ): void {
    this.push(this.generateCSV(telemetry));
    callback();
  }

  private generateCSV(object: Record<string, unkwnown>): string {
    let csv = '';

    if (!this.hasSetHeaders) {
      csv += this.createCSVHeaders(object);
      this.hasSetHeaders = true;
    }

    csv += this.createRecord(object);

    return csv;
  }

  private createCSVHeaders(object: Record<string, unkwnown>): string {
    return `${Object.keys(object)}\n`;
  }

  private createCSVRecord(object: Record<string, unkwnown>): string {
    return `${Object.values(object)}\n`;
  }
}

Jag implementerade följande testfall (med jest) för att testa att givet ett flöde av enkla objekt av "samma typ" är den förväntade utmatningen en giltig CSV-representation av dem:

describe('object to csv stream', () => {
  const items: Record<string, unknown> = [
    { foo: 1, bar: 2, baz: 3 },
    { foo: 10, bar: 20, baz: 30 },
    { foo: 100, bar: 200, baz: 300 },
  ];

  it('should transform a list of items to csv', (done) => {
    const expectedCsv = 'foo,bar,baz\n1,2,3\n10,20,30\n100,200,300\n';
    let csv = '';

    Readable.from(items)
      .pipe(ObjectToCSVTransform.create())
      .on('data', (csvResult) => {
        csv += csvResult;
        console.log(csvResult); // just for debugging purposes
      })
      .on('end', () => {
        console.log('streaming ended'); // just for debugging purposes
        expect(csv).toEqual(expectedCsv);
        done();
      });
  });
});

Tydligen, under testfallimplementeringen ville jag se hur mitt testfall misslyckades: strömmen verkade fungera eftersom den loggade varje förväntad csv-resultatrad och även 'streaming ended'-meddelandet i slutet när strömmen avslutades, men testkörningen avslutades då inte alls. Det var Jest som faktiskt avslutade körningen efter att ha överskridit standardtimeouten:

kastad: "Överskred timeout på 5000 ms för ett test. Använd jest.setTimeout(newTimeout) för att öka timeoutvärdet, om detta är ett långvarigt test."

Vad missar jag här?

Observera att jag för närvarande använder jest-callbacken för att meddela när den asynkrona operationen har slutförts.

Svar #1

Det finns något som jag saknade i mitt testfall som inte hanterade asynkronitet korrekt alls och därför fick Exceeded timeout exception att kastas av jest:

  • omge påståendet med en try-catch och låta done() callbacken anropas om påståendet misslyckades.
.on('end', () => {
  try {
    expect(csv).toEqual(expectedCsv);
    done();
  } catch (error) {
    done(error);
  }
});

För mer information/referens, kolla jest-dokumentationen: https://jestjs.io/docs/asynchronous#callbacks

Svar #2

Problemet är att det returnerade värdet av csv inte matchar det förväntade värdet. Det faktiska returnerade värdet inkluderar en \n i slutet. Om du lägger till detta i din expectedCsv-sträng kommer ditt test att gå igenom.

Timeout-fel är en vilseledande ledtråd.

Om du console.log(csv.replace(/\n/g, ''\\n'')) i funktionen 'end' kommer du att se den extra radbrytningen i slutet av strängen.

import { Readable, Transform, TransformCallback } from 'stream';

class ObjectToCSVTransform extends Transform {
  private hasSetHeaders: boolean;

  constructor() {
    super({ objectMode: true });
    this.hasSetHeaders = false;
  }

  static create(): ObjectToCSVTransform {
    return new this();
  }

  // eslint-disable-next-line no-underscore-dangle
  _write(
    object: Record<string, unknown>,
    _encoding: 'utf8',
    callback: TransformCallback,
  ): void {
    this.push(this.generateCSV(object));
    callback();
  }

  private generateCSV(object: Record<string, unknown>): string {
    let csv = '';

    if (!this.hasSetHeaders) {
      csv += this.createCSVHeaders(object);
      this.hasSetHeaders = true;
    }

    csv += this.createCSVRecord(object);

    return csv;
  }

  private createCSVHeaders(object: Record<string, unknown>): string {
    return `${Object.keys(object)}\n`;
  }

  private createCSVRecord(object: Record<string, unknown>): string {
    return `${Object.values(object)}\n`;
  }
}

describe('object to csv stream', () => {
  const items: Record<string, unknown>[] = [
    { foo: 1, bar: 2, baz: 3 },
    { foo: 10, bar: 20, baz: 30 },
    { foo: 100, bar: 200, baz: 300 },
  ];

  it('should transform a list of items to csv', (done) => {
    const expectedCsv = 'foo,bar,baz\n1,2,3\n10,20,30\n100,200,300\n';
    let csv = '';

    Readable.from(items)
      .pipe(ObjectToCSVTransform.create())
      .on('data', (csvResult) => {
        csv += csvResult;
        console.log(csvResult); // just for debugging purposes
      })
      .on('end', () => {
        console.log('streaming ended'); // just for debugging purposes
        console.log(csv.replace(/\n/g, '\\n'));
        expect(csv).toEqual(expectedCsv);
        done();
      });
  });
});