错误:无法解析 (?, ?) 的所有参数

问题描述

我为我的 Angular 10 组件编写了以下单元测试,它基本上显示了具有一些交互性的树视图:

import { ComponentFixture,Testbed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
// import { MatFormFieldModule } from '@angular/material/form-field';
// import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressspinnerModule } from '@angular/material/progress-spinner';
import { browserModule } from '@angular/platform-browser';
import { TreeviewModule } from 'ngx-treeview';
import { DocumentTreeService } from '../services/DocumentTreeService';

import { DocumentTreeviewComponent } from './document-treeview.component';

describe('DocumentTreeviewComponent',() => {
  let component: DocumentTreeviewComponent;
  let fixture: ComponentFixture<DocumentTreeviewComponent>;

  beforeEach(async () => {
    await Testbed.configureTestingModule({
      declarations: [ DocumentTreeviewComponent ],imports: [ TreeviewModule.forRoot(),ReactiveFormsModule,MatProgressBarModule,browserModule,MatProgressspinnerModule,/* MatFormFieldModule,MatInputModule */ ],providers: [ DocumentTreeService ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = Testbed.createComponent(DocumentTreeviewComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create',() => {
    expect(component).toBeTruthy();
  });
});

我的 DocumentTreeService.ts:

import { HttpClient } from '@angular/common/http';
import { TreeviewItem } from 'ngx-treeview';
import { globalDataService } from './global-data.service';

interface TreeItem {
  name?: string;
  text: string;
  value: any;
  children?: String[];
  type: 'folder' | 'document';
}

/* tslint:disable no-string-literal prefer-for-of */
export class DocumentTreeService {

  constructor(public http: HttpClient,public DataService: globalDataService){}
  public treeviewmtextresponse;
  public docNames = [];

  public FolderItems = [];
  public treeviewItem = [];
  public finalTreeviewElements: TreeviewItem[] = [];

  getDocItems(): TreeviewItem[] {
    const docItems = [];

    // TEMPORARY SOLUTION (QUICKFIX) FOR 1 DOC
    if (!this.treeviewmtextresponse.length) {
      const element = this.treeviewmtextresponse['documentName'];
      this.docNames.push(element);
      const tempArray = element.split('!');
      if (!this.FolderItems.includes(tempArray[0])) {
        this.FolderItems.push(tempArray[0])
      }

      docItems[tempArray[0]] = this.docNames.filter(s => s.includes(tempArray[0] + '!'));
      for (let o = 0; o < docItems[tempArray[0]].length; o++) {
        docItems[tempArray[0]][o] = docItems[tempArray[0]][o].replace(tempArray[0] + '!','');
        const documentItem: TreeItem = {
          text: docItems[tempArray[0]][o],type: 'document',value: docItems[tempArray[0]][o],}
        docItems[tempArray[0]][o] = documentItem;
      }
    }
    // TEMPORARY SOLUTION (QUICKFIX) FOR 1 DOC

    for (let m = 0; m < this.treeviewmtextresponse.length; m++) {
      const element = this.treeviewmtextresponse[m]['documentName'];
      this.docNames.push(element);
      const tempArray = element.split('!');
      if (!this.FolderItems.includes(tempArray[0])) {
        this.FolderItems.push(tempArray[0])
      }

      docItems[tempArray[0]] = this.docNames.filter(s => s.includes(tempArray[0] + '!'));
      for (let o = 0; o < docItems[tempArray[0]].length; o++) {
        docItems[tempArray[0]][o] = docItems[tempArray[0]][o].replace(tempArray[0] + '!',}
        docItems[tempArray[0]][o] = documentItem;
      }
    }

    let jsonString;
    for (let p = 0; p < this.FolderItems.length; p++) {
      const element = this.FolderItems[p];
      const treeItem: TreeItem = {
        name: element + "!" + docItems[element][0]["text"],text: element,value: element,type: 'folder',children: docItems[element],}
      const DokData = treeItem["name"];
      this.DataService.SelectedDocumentList.push(DokData);

      jsonString = JSON.stringify(treeItem);
      const finalObject: TreeviewItem = new TreeviewItem(
        JSON.parse(jsonString));
      this.finalTreeviewElements.push(finalObject);
    }
    return this.finalTreeviewElements;
    }
}

我的完整错误信息:

Error: Can't resolve all parameters for DocumentTreeService: (?,?).

我最初认为我把服务放在正确的类别中是一团糟,但经过反复试验和一些逻辑,我确定它设置正确。导致此问题的原因是什么?


更新 1

我当前的代码库如下所示:

 beforeEach(async () => {
    const httpSpy = jasmine.createSpyObj('HttpClient',['get']); //funcName for any function name that you need to exist on the spy objects
    const dataSpy = jasmine.createSpyObj('globalDataService',['funcName']);
    await Testbed.configureTestingModule({
      declarations: [ DocumentTreeviewComponent ],HttpClientTestingModule,MatDialogModule,MatInputModule
         /* MatFormFieldModule,providers: [ globalDataService,DatePipe,{ provide: DocumentTreeService,useValue: mockDocumentTreeService },{ provide: HttpClient,useValue: httpSpy },{ provide: globalDataService,useValue: dataSpy }] // modify this line to provide a mock
    })
    .compileComponents();
       // To access the service from your tests
    documentTreeService = Testbed.inject(DocumentTreeService);
  });

更新 2

文档-treeview.component.ts:

import { Component,OnInit } from '@angular/core';
import { TreeviewItem,TreeviewConfig } from 'ngx-treeview';
import { globalDataService } from '../services/global-data.service';
import { DocumentTreeService } from '../services/DocumentTreeService';
import { environment } from 'src/environments/environment';
@Component({
  selector: 'app-document-treeview',templateUrl: './document-treeview.component.html',styleUrls: ['./document-treeview.component.scss']
})

export class DocumentTreeviewComponent implements OnInit {
  dropdownEnabled = false;
  items: TreeviewItem[];
  values: number[];
  config = TreeviewConfig.create({
    hasAllCheckBox: false,hasFilter: true,hasCollapseExpand: true,maxHeight: 435
  });

  constructor(public service: DocumentTreeService,public DataService: globalDataService) {
    this.getDocumentList(false);
   }

  public docAmount;

  ngOnInit(): void {
    setInterval(()  => {
      if (!this.items && this.service.treeviewmtextresponse) {
        if (this.service.finalTreeviewElements.length !== 0) {
          this.items = this.service.finalTreeviewElements;
        } else {
            this.items = this.service.getDocItems();
        }
      }
      if (this.DataService.newDocumentAdded === true || this.DataService.documentDeleted === true) {
        this.resetDocuments();
        if (this.service.treeviewmtextresponse.length > this.docAmount && this.DataService.newDocumentAdded === true || this.service.treeviewmtextresponse.length < this.docAmount && this.DataService.documentDeleted === true) {
          this.DataService.newDocumentAdded = false;
          this.DataService.documentDeleted = false;
          this.docAmount = this.service.treeviewmtextresponse.length;
        }
      }
    },1000);
  }

  public resetDocuments() {
    setTimeout(() => {
      // reset treeview arrays
      this.items = null;
      this.service.docNames = [];
      this.service.FolderItems = [];
      this.service.treeviewItem = [];
      this.service.finalTreeviewElements = [];
      // reset treeviewmtextresponse
      this.service.treeviewmtextresponse = null;
      this.getDocumentList(true);
  },1000);
  }

  onFilterChange(value: string): void {
    console.log('filter:',value);
  }

  public getFolderName(event) {
    // irrelevant for the unit test
  }

  openTonicUrl(event){
    // this.openDialog(event);
    if (this.DataService.SelectedDocumentName) {
      window.open(`${environment.APIUrl}/text/guI/Open/%5Chome%5Ctestproject%5C` + this.DataService.SelectedDocumentName,'_blank');
    }
  }

  public getDocumentList(refresh: boolean) {
    return this.service.http.get(this.DataService.getDocumentUrl,this.DataService.httpOptions)
    .subscribe(
      documentResponse => {
        this.service.treeviewmtextresponse = documentResponse['soap:Envelope']['soap:Body'][
          'ns2:getDocumentsResponse'
        ]['return'];
        if (refresh === false) {
          this.docAmount = this.service.treeviewmtextresponse.length;
        }
        this.DataService.documentListLoaded = true;
       },error => {

        console.log('There was an error: ',error);
      });
  }
}

解决方法

您的 DocumentTreeService 正在使用 HttpClientGlobalDataService,而您没有在 TestBed 模块中提供它们。在测试组件时,我会模拟 DocumentTreeService 而不会提供实际服务。

describe('DocumentTreeviewComponent',() => {
  let component: DocumentTreeviewComponent;
  let fixture: ComponentFixture<DocumentTreeviewComponent>;
  let mockDocumentTreeService = jasmine.createSpyObj('documentTreeService',['getDocItems']); // add this line,also look into how to mock external dependencies such as a service  
// !! Remove the <DocumentTreeService> on the above line !!

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ DocumentTreeviewComponent ],imports: [ TreeviewModule.forRoot(),ReactiveFormsModule,MatProgressBarModule,BrowserModule,MatProgressSpinnerModule,/* MatFormFieldModule,MatInputModule */ ],providers: [{ provide: DocumentTreeService,useValue: mockDocumentTreeService }] // modify this line to provide a mock
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(DocumentTreeviewComponent);
    component = fixture.componentInstance;
    mockDocumentTreeService.getDocItems().and.returnValue([]); // we have mocked getDocItems to return an empty array. You can return anything you want for that function.
    fixture.detectChanges();
  });

  it('should create',() => {
    expect(component).toBeTruthy();
  });
});
,

服务需要为类使用@Injectable() 装饰器。

例如

@Injectable({providedIn: 'root'}) //if you want to provide it in a certain module scope. Remove the `providedIn`
export class DocumentTreeService {...

如果这不能自行解决。下面是注入具有依赖项的服务的另一种方式。

let documentTreeService: DocumentTreeService;
beforeEach(async () => {
    for any function name that you need to exist on the spy objects
    const dataSpy = jasmine.createSpyObj('GlobalDataService',['funcName']);
    await TestBed.configureTestingModule({
      //... removed for brevity
      imports: [HttpClientTestingModule,...],providers: [
           DocumentTreeService,{ provide: GlobalDataService,useValue: dataSpy }
      ]
    })
    .compileComponents();
   //To access the service from your tests
      documentTreeService = TestBed.inject(DocumentTreeService);
  });
,

由于您正在编写单元测试,因此您只想测试组件而不是其任何依赖项。最理想的是,您可以 Mock 声明的所有其他组件以及所有提供程序。

一个很好的库是 ng-mocks。然后,您不必被迫检查您的服务和您的组件,而只需模拟它即可。

providers: [ DocumentTreeService ]

然后可以更改为

providers: [ {
    provide: DocumentTreeService,useValue: MockService(DocumentTreeService)
}]

这样您就可以将外部服务与组件的单元测试分开