Welcome to 16892 Developer Community-Open, Learning,Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

BACKGROUND

I have an Angular project that has a web API we use for back-end web requests, but now we need to transition to a different API provider.

The API requests are all integrated into Angular service code files, and each service exists to fulfill a specific responsibility.

What myself and my fellow devs are tasked to do are create new, "Version 2" services, with the same method signatures and parameters, they must call the second new API.

Are goal is to create new services that have the same methods and signatures taking exactly the same parameters and yielding the same results. However, the API method signatures on the servers are often slightly different, and some of those results coming from the new API may need to be modified and massaged to fit the original format.

SETUP

In order to aid in this transition, we are wanting to create slightly less-traditional Jasmine unit tests.

For each service being transitioned, we intend to create a ...spec.ts file that will execute the following, high-level requirements for testing and debugging.

  1. Initialize the test and using TestBed.configureTestingModule, setup appropriate providers for an old service and a new service.
  2. For each method on the services returning results from the server, run an it(...) test.
  3. Within the test, first call MyService.apiRequest(). Get the observable and get the result.
  4. Call an expect(...) the old result set to be truthy for a sanity check.
  5. Call MyServiceV2.apiRequest(). Get the observable and get the result.
  6. expect(...) the new result set to be truthy.
  7. Construct an expect(...) statement, double-checking that the results from the old API match the new API results.

To be clear, these are NOT tests that we intend to use in a CI/CD pipeline or to double-check out code. These are Jasmine tests that we are using to quickly inspect the result sets from the various APIs we have to work with.

PROBLEM

I have tried various means of setting up tests like this but I cannot seem to get the results back from our services, wait on the observables and compare the data. A lot of Jasmine documentation I've found expects you to fake results through mocks when working with observables. This cannot work in our use case.

In our case, our service calls are legit, and are a means of double-checking our data results from the two specific API endpoints.

For full disclosure, the application is currently stuck at Angular 8.2.14 and Jasmine 2.8.0. Listed below are the various attempts I've made to get this to work.

Attempt #1

it('should get data from MyService.getData(projectId)', ()=>{

        //oldSvc and newSvc are references to the old and new services, initialized in beforeEach(...)

        //Call to get the first round of data (old data.)
        oldSvc.getData(projectId).subscribe((oldData)=>{
            expect(oldData).toBeTruthy();
            newSvc.getData(projectId, false).subscribe((newData)=>{
                expect(newData).toBeTruthy();
                expect(newData).toEqual(oldData);
            });
        });
    });

This returns a Jasmine success but it seems to be that this is because "SPEC HAS NO EXPECTATION". I guess the test finishes before the observable subscribers get a chance to finish, much less call the expect(...) methods to make any checks against the data. Ergo, it looks successful, but it's not really doing anything.

Attempt #2

Questions like this one recommend using the first() method to wait and take the first result from each asynchronous request. I have nothing against this approach, but in spite of every effort I've made, I get errors that first() does not exist. I've added references to first (see here for an SO question with instruction on how to do this) but this does not fix the issue. Even after adding the import, first() does not exist, I get build errors and I had to abandon this approach. I even tried to ignore the error with special TS flags, and instead of getting build errors, I get first() does not exist related errors from within Jasmine. This was a non-starter.

Attempt #3

I found this article mentioning that you can wait on the result and use the jasmine.clock.tick(...) method to wait results. By integrating the method taken from the article into your test file, you can create "synchronous" requests to your observables. Great! ...but this didn't work. My results are always undefined, proving that it in fact does not wait for the results, even with generous clock wait times.

Here is the meat of the article and a sample call to this method.

//Method
function awaitStream(stream$: Observable<any>, skipTime?: number) {
  let response = null;
  stream$.subscribe(data => {
    response = data;
  });
  if (skipTime) {
    /**
     * use jasmine clock to artificially manipulate time-based web apis like setTimeout and setInterval
     * we can easily refactor this and use async/await but that means that we will have to actually wait out the time needed for every delay/mock request
     */
    jasmine.clock().tick(skipTime);
  }
  return response;
}

//Usage within an it(...) method

...
const oldData = awaitStream(oldSvc.getData(projectId), 10000);
//after calling the method, the result of the observable should be in oldData.  It isn't.
...

QUESTION

These are the various attempts I've made to get asynchronous data from API calls in Jasmine for usage and testing. I can't get any of this to work under our current Angular/Jasmine version restrains. Does anyone have a straight-forward method for calling and waiting on data coming from a service returning Observables, and inspecting the results in Jasmine?

Thank you.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
248 views
Welcome To Ask or Share your Answers For Others

1 Answer

This may not answer your question but guide you in the right direction.

What is strange to me is that you seem to want to make actual HTTP calls (from what I gather) and this is not recommended for unit testing. But I also see your point that if we mock the Http calls or service calls, there will be no point in testing.

You can try using async/await to accomplish what you want.

import { take } from 'rxjs/operators';
....
it('should get data from MyService.getData(projectId)', async ()=>{

        //oldSvc and newSvc are references to the old and new services, initialized in beforeEach(...)
        const oldData = await oldSvc.getData(projectId).pipe(take(1)).toPromise();
        expect(oldData).toBeTruthy();
        const newData = await newSvc.getData(projectId, false).pipe(take(1)).toPromise();
        expect(newData).toBeTruthy();
        expect(newData).toEqual(oldData); 
    });

Or you could have used the done callback in your original test:

it('should get data from MyService.getData(projectId)', (done)=>{

        //oldSvc and newSvc are references to the old and new services, initialized in beforeEach(...)

        //Call to get the first round of data (old data.)
        oldSvc.getData(projectId).subscribe((oldData)=>{
            expect(oldData).toBeTruthy();
            newSvc.getData(projectId, false).subscribe((newData)=>{
                expect(newData).toBeTruthy();
                expect(newData).toEqual(oldData);
                done(); // finish test when done is called to ensure it goes through all assertions
            });
        });
    });

That being said, I recommend mocking your Http responses so it doesn't hammer your actual API. Check out this article. You can use HttpClientTestingModule and HttpTestingController to have a handle on the in flight HTTP requests by url or method (GET, PUT, POST, DELETE) and flush a specific response for each request.

============= Edit ========

1st question - Something like this:

let result: any; // you can change any to what you want
beforeEach(async (done) => {
  result = await yourSvc.getData().pipe(take(1)).toPromise();
  done();
});

it('should do xyz', () => {
  console.log(result); // should have a handle on result now
});

2nd question - Done is not supposed to be imported. It is a stock feature from Jasmine. Basically, we can call it anything but it has to be the first argument for the test.

it('should do xyz', (anyRandomNameYouWantHere: DoneFn) => {
  someAsyncTask.then(() => {
   // your assertions
   anyRandomNameYouWantHere(); // call the first argument to tell Jasmine you are done
  });
});

To be semantic, you should call anyRandomNameYouWantHere as done and the type is DoneFn but it shouldn't be necessary to put the type there. You can learn more about done here.

To get the first operator, you have to do import { first } from 'rxjs/operators'; but if you're getting errors, you should check out tsconf.spec.json to ensure you have the accurate types of Jasmine and other modules. Check how the tsconf.spec.json of a blank project looks like and compare it to yours and make some changes. You may have to restart your IDE.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to 16892 Developer Community-Open, Learning and Share
...