Можете ли вы дать мне отзыв о моих недавних модульных тестах с angular и Жасмин? Я заинтересован в том, чтобы развить свои навыки в отношении написания лучших модульных тестов.
Должен ли мой тест быть в e2e? Могу ли я написать больше модульных тестов для этого компонента? Должен ли я сочетать некоторые из костюмов?
Любой отзыв будет принят!
Спасибо!
component.ts :
export class ReportDatasetCreationComponent implements OnInit, AfterViewChecked, OnDestroy {
options: TableOptions = {
sortable: true,
filterable: true,
navigateToEnd: true,
callEvent: () => {
this.data = [...this.data, { id: '', desc: '', type: 'str', values: '' }];
this.inputsValid = true;
},
displayHeader: true,
autoHeight: true,
pagination: {
length: 50,
pageSize: 50,
pageSizeOptions: [50, 100, 150, 200],
}
};
loading = true;
data: TableData = [];
mainData;
inputsValid;
existingDatasets;
columns = [];
datasetDictionaries;
// dictionaries;
disableData;
dataChanged = false;
baseData;
name = this.route.snapshot.params.reportName;
duplicatedReport;
show = false;
showOriginRdsName = true;
readOnly: boolean;
tableDataObjChanged = false;
indexOfElementToDelete;
deleteHierarchyResponse;
columnLinkedHierarchies = [];
constructor(
private route: ActivatedRoute,
public dialog: MatDialog,
public snackBar: MatSnackBar,
private router: Router,
private reportDatasetsService: ReportDatasetsService,
public dictionaryService: DictionaryService,
public datasetsService: DatasetsService,
private authenticationService: AuthenticationService,
public notificationsService: NotificationsService
) {
}
getColumns() {
return [
{
key: 'id', label: 'Column', type: TABLE_COLUMNS_TYPES.INPUT, config: {
validators: [this.isEmpty, this.isNotUnique],
disabled: this.isDisabled
}
},
{
key: 'desc', label: 'Description', type: TABLE_COLUMNS_TYPES.INPUT, cellStyles: { 'width': '30%' }, config: {
disabled: this.isDisabled
}
},
{
key: 'type', label: 'Type', type: TABLE_COLUMNS_TYPES.ROW_SELECT, options: this.datasetDictionaries, config: {
validators: [this.isEmpty],
disabled: this.isDisabled
}
},
{
key: 'values', label: 'Applicable Values', type: TABLE_COLUMNS_TYPES.CHIPS, config: {
disabled: this.isDisabled,
isHierarchy: (row) => {
return row.hierarchy ? true : false;
}
}
},
{
key: 'manage-app-values',
label: '',
type: TABLE_COLUMNS_TYPES.ACTION,
icon: 'sort',
onClick: (element) => {
if (!this.readOnly) { this.openHierarchyModal(element); }
},
config: {
disabled: this.isDisabled,
isRender: (row) => {
return !this.isDisabled(row);
}
},
},
{
key: 'delete',
label: '',
type: TABLE_COLUMNS_TYPES.ACTION,
icon: 'delete',
onClick: (element) => {
if (!this.readOnly) { this.deleteRow(element); }
},
config: {
disabled: this.isDisabled,
isRender: (row) => {
return !this.isDisabled(row);
}
},
}
];
}
isNotUnique = (row, column) => {
if (this.isDisabled(row)) {
return null;
}
const value = row[column.key];
const duplicate = this.data.find((dataRow: TableRow) => {
return row !== dataRow && dataRow[column.key] === value;
});
return duplicate ? 'this field should be unique' : null;
}
isEmpty = (row, column) => {
if (this.isDisabled(row)) {
return null;
}
const value = row[column.key];
return Boolean(value) ? null : '*required';
}
isDisabled = (row) => {
return this.disableData.includes(row);
}
ngOnInit() {
this.authenticationService.readOnly.pipe(first()).subscribe(res => {
this.readOnly = res;
});
this.datasetDictionaries = this.dictionaryService.getDictionary('ReportDataSetDefinition.col_dtypes');
this.columns = this.getColumns();
if (!this.reportDatasetsService.isNewRDS) {
this.getReportDatasetDetails();
}
this.reportDatasetsService.$reportDatasetDetails.subscribe((reportDatasetDetails: ReportDataset) => {
if (reportDatasetDetails) {
if (reportDatasetDetails['data_in_db_dt'] && reportDatasetDetails['data_in_db_dt'] !== 'NaT') {
this.reportDatasetsService.data_in_db = true;
} else { this.reportDatasetsService.data_in_db = false; }
this.data = Object.assign(reportDatasetDetails.columns);
this.disableData = this.data.filter((item: any) => {
return DISABLED_ROWS_IDS.includes(item.id);
});
this.loading = false;
}
});
this.reportDatasetsService.loadingStatus
.subscribe(loadingStatus => {
this.loading = !isResolved(loadingStatus);
});
this.duplicatedReport = this.reportDatasetsService.reportDatasetDetails;
if ((!this.duplicatedReport) || (!this.duplicatedReport.hasOwnProperty('newName'))) {
this.show = false;
this.showOriginRdsName = true;
this.duplicatedReport = null;
} else {
this.show = true;
this.showOriginRdsName = false;
}
}
ngAfterViewChecked() {
}
ngOnDestroy() {
this.show = false;
this.showOriginRdsName = true;
if (this.tableDataObjChanged) {
this.onSave();
}
}
getReportDatasetDetails() {
this.reportDatasetsService.getReportDatasetDetails(this.name).subscribe();
}
downloadStructure() {
this.reportDatasetsService.downloadRDSStructure(this.name).subscribe(data => saveAs(data, this.name + '.csv'));
}
uploadStructure() {
const dialogRef = this.dialog.open(UploadDatasetStructureComponent, {
width: '700px',
data: {
datasetInfo: {
name: this.route.snapshot.params.reportName
},
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
const file = result['file'];
const delimiter = result['delimiter'];
const reader: FileReader = new FileReader();
reader.readAsText(file);
reader.onload = (e) => {
const csv: string = reader.result as string;
const lines = csv.split('\n');
const array = [];
for (let i = 1; i < lines.length; i++) {
let arr = [];
let line = lines[i].trim();
let lastIndex = null;
if (line !== '') {
if (delimiter === ',') {
if (line[line.length - 1] === ']') {
for (let j = line.length - 2; j > 0; j--) {
if (line[j] === '[') {
lastIndex = j;
line = line.slice(0, lastIndex) + '"' + line.slice(lastIndex) + '"';
break;
}
}
}
if (line[line.length - 1] === '"') {
for (let k = line.length - 2; k > 0; k--) {
if (line[k] === '"') {
lastIndex = k;
arr[3] = line.substring(lastIndex + 1, line.length - 1);
arr[3] = JSON.parse(arr[3].replace(/'/g, '"'));
line = line.substring(0, lastIndex - 1);
break;
}
}
} else {
arr[3] = '';
line = line.substring(0, line.length - 1);
}
for (let l = line.length - 1; l > 0; l--) {
if (line[l] === ',') {
lastIndex = l;
arr[2] = line.substring(lastIndex + 1, line.length);
line = line.substring(0, lastIndex);
break;
}
}
lastIndex = line.indexOf(',');
arr[0] = line.substring(0, lastIndex);
arr[1] = line.substring(lastIndex + 1, line.length);
if (arr[1].indexOf(',') > -1) {
arr[1] = arr[1].substring(1, arr[1].length - 1);
}
} else {
arr = lines[i].split('|');
arr[3] = arr[3].trim();
if (arr[3] !== '') {
arr[3] = JSON.parse(arr[3].replace(/'/g, '"'));
}
}
const obj = {
id: arr[0],
desc: arr[1],
type: arr[2],
values: arr[3]
};
array.push(obj);
}
}
this.data = array;
this.disableData = this.data.filter((item: any) => {
return DISABLED_ROWS_IDS.includes(item.id);
});
this.setDataChanged();
};
}
});
}
deleteRow(element) {
this.dialog.open(DeleteConfirmationComponent).afterClosed().subscribe((isConfirmed: boolean) => {
if (isConfirmed) {
this.reportDatasetsService.saveReportDatasetDetails(this.data.filter((row) => row !== element)).subscribe(res => {
if (!(res instanceof HttpErrorResponse)) {
this.data = this.data.filter((row) => row !== element);
this.resetBaseData();
this.openSnackBar();
}
});
}
});
}
rowChange(e) {
const isNotEmpty = disableButton(this.data, 'id');
const dublicated = checkDuplicateInObject('id', this.data);
if (isNotEmpty || dublicated) {
this.inputsValid = true;
} else {
this.inputsValid = false;
}
this.setDataChanged();
}
openSnackBar() {
const snackBarRef = this.snackBar.open('Report Dataset has been successfully saved', 'go back to RDS list', {
duration: 2000
});
snackBarRef.onAction().subscribe(() => {
this.router.navigate(['designer/report-datasets/']);
});
}
goBack() {
this.reportDatasetsService.isNewRDS = false;
this.router.navigate(['designer/report-datasets/']);
}
onSave() {
this.reportDatasetsService.saveReportDatasetDetails(this.data).subscribe(res => {
if (!(res instanceof HttpErrorResponse)) {
this.resetBaseData();
this.openSnackBar();
}
});
}
openUploadModal() {
this.dialog.open(UploadDatasetComponent, {
width: '400px',
data: {
datasetInfo: {
name: this.route.snapshot.params.reportName
},
isReportDataset: true
}
});
}
downloadFile() {
this.reportDatasetsService.checkStatusCsvFile(this.name).subscribe((res) => {
if (!(res instanceof HttpErrorResponse)) {
this.reportDatasetsService.downloadRDScsvData(this.name).subscribe(data => saveAs(data, this.name + '.csv'));
}
});
}
setDataChanged() {
this.dataChanged = !isEqual(this.data, this.baseData);
}
resetBaseData() {
this.baseData = cloneDeep(this.data);
this.setDataChanged();
}
iconContent(e) {
if (e.fromElement === null) {
} else if (e.fromElement.textContent === 'delete') {
this.datasetsService.toolTipContent = 'Delete';
} else if (e.fromElement.textContent === 'sort') {
this.datasetsService.toolTipContent = 'Link/unlink hierarchy';
}
}
openHierarchyModal(el) {
const dialogRef = this.dialog.open(ManageApplicableValuesComponent, {
width: '90%',
data: {
element: el,
rdsName: this.route.snapshot.params.reportName,
columnName: el.id,
fromAction: 'rdsColumn'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result === undefined) {
// User clicked cancel or focus out from the modal
} else if (result.action === 'save') {
for (let i = 0; i < this.data.length; i++) {
if (this.data[i].hierarchy === result.name) {
const applicableValues = result.rowData.map(a => a.name);
this.data[i].values = applicableValues;
}
}
this.tableDataObjChanged = true;
this.setDataChanged();
} else if (result.action === 'unPairHierarchy') {
this.data.find((o, i) => {
if (o.id === el.id) {
delete this.data[i].hierarchy;
return true; // stop searching
}
});
this.snackBar.open('Hierarchy un linked from column', null, { duration: 1000 });
this.tableDataObjChanged = true;
}
});
}
}
spe c .ts file:
fdescribe('ReportDatasetCreationComponent', () => {
let component: ReportDatasetCreationComponent;
let fixture: ComponentFixture<ReportDatasetCreationComponent>;
let service;
let mySpy;
let httpMock;
let de: DebugElement;
let mockData = {
columns: [
{
desc: 'Unique identifier of event. (Mandatory)',
id: 'id',
type: 'str'
},
{
id: 'CURRENCY',
type: 'str'
},
{
id: 'NAME',
type: 'str'
},
{
hierarchy: 'coockie',
id: 'COUNTRY',
type: 'str',
values: [
'a',
'b',
'e',
'123456789123456789123456789123456789123456789123456789123456789123456789'
]
},
{
hierarchy: 'q',
id: 'PRICE',
type: 'nbr',
values: [
'q',
'1',
'2'
]
}
],
data_in_db_dt: '2020-01-06T23:21:46.375819',
desc: '',
filter_criteria: {},
name: 'motoDealRds',
updt_by: 'Shubby dubby',
updt_on: '2020-01-06T23:21:46.376801'
};
let mockMusicShop = {
columns: [
{
desc: 'Unique identifier of event. (Mandatory)',
id: 'id',
type: 'str'
},
{
id: 'product',
type: 'str'
},
{
id: 'price',
type: 'nbr'
},
{
id: 'currency',
type: 'str'
},
{
id: 'manufacturer',
type: 'str'
}
],
data_in_db_dt: '2020-02-12T19:03:16.755805',
desc: 'RDS for unit testings',
filter_criteria: {},
name: 'music_shop',
updt_by: 'Nikhil',
updt_on: '2020-02-12T19:03:16.755805'
}
let mockColumnElement = {
hierarchy: 'company_hierarchy_mock',
id: 'PRICE',
type: 'nbr',
values: ['q', '1', '2']
};
let mockHierarchy = {
name: 'mock hierarchy',
rowData: [
{ name: 'a', path: ['a'], type: 'folder', id: 1 },
{ name: 'b', path: ['a', 'b'], type: 'folder', id: 2 },
{ name: 'c', path: ['a', 'c'], type: 'folder', id: 3 },
{ name: 'd', path: ['a', 'd'], type: 'folder', id: 4 }
]
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [DesignerModule, RouterTestingModule, HttpClientTestingModule],
providers: [
AuthenticationService,
DictionaryService,
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (mockBackend: MockBackend, defaultOptions: RequestOptions) => {
return new Http(mockBackend, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]
}
]
})
.compileComponents();
}));
beforeEach(() => {
// ReportDatasetCreationComponent.prototype.ngOnInit = () => { };
fixture = TestBed.createComponent(ReportDatasetCreationComponent);
component = fixture.componentInstance;
component.dictionaryService = new DictionaryServiceMock(TestBed.get(HttpClient));
service = TestBed.get(ReportDatasetsService);
httpMock = TestBed.get(HttpTestingController);
de = fixture.debugElement;
component.name = 'motoDealRds';
mySpy = spyOn(service, 'getReportDatasetDetails').and.callThrough();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should spyOn getReportDatasetDetails and return fake data', () => {
service.$reportDatasetDetails.next(mockMusicShop);
// this function should be called in ngOnInit once
expect(service.getReportDatasetDetails).toHaveBeenCalledTimes(1);
// Should enter the if condition after the response
expect(component.reportDatasetDetailsResponse).toEqual(jasmine.objectContaining({
data_in_db_dt: '2020-02-12T19:03:16.755805'
}));
// Since the response contain data, the data_in_db should be true:
// expect(service.data_in_db).toBeTruthy();
});
it('Should return different results since the mockData will be change', () => {
// Now, let's try to changed the data:
service.$reportDatasetDetails.next(undefined);
// this function should be called in ngOnInit once
expect(service.getReportDatasetDetails).toHaveBeenCalledTimes(1);
// The if condition should not pass and should not pass and go to the else scope:
expect(service.data_in_db).toBeFalsy();
});
// mock a duplicate report and test the if conditions and the variables inside
it('Should test the if conditions and the variables inside', () => {
component.duplicatedReport = mockMusicShop;
expect(component.show).toBeFalsy();
expect(component.showOriginRdsName).toBeTruthy();
expect(component.show).toBeFalsy();
});
it('Should call ngOnDestroy and test the state of the variables', () => {
component.ngOnDestroy();
expect(component.show).toBeFalsy();
expect(component.showOriginRdsName).toBeTruthy();
expect(component.tableDataObjChanged).toBeFalsy();
});
it('Should call ngOnDestroy and test the state of the variables, but this time, expect to call onSave once', () => {
service.reportDatasetDetails = mockMusicShop;
component.tableDataObjChanged = true;
const onSaveSpy = spyOn(component, 'onSave').and.callThrough();
component.ngOnDestroy();
expect(onSaveSpy).toHaveBeenCalledTimes(1);
});
it('Should call onSave with mock data', () => {
service.$reportDatasetDetails.next(mockMusicShop);
service.reportDatasetDetails = mockMusicShop;
const onSaveSpy = spyOn(component, 'onSave').and.callThrough();
const saveReportDatasetDetailsSpy = spyOn(service, 'saveReportDatasetDetails').and.returnValue(of(null));
const resetBaseDataSpy = spyOn(component, 'resetBaseData').and.callThrough();
const openSnackBarSpy = spyOn(component, 'openSnackBar').and.callThrough();
component.onSave();
expect(onSaveSpy).toHaveBeenCalledTimes(1);
expect(saveReportDatasetDetailsSpy).toHaveBeenCalledTimes(1);
// Since the response is not en error object, it should call resetBaseData and openSnackBar
expect(resetBaseDataSpy).toHaveBeenCalledTimes(1);
expect(openSnackBarSpy).toHaveBeenCalledTimes(1);
});
it('Should call onSave with mock data, but this time the response will not pass the if condition', () => {
service.$reportDatasetDetails.next(mockMusicShop);
service.reportDatasetDetails = mockMusicShop;
const onSaveSpy = spyOn(component, 'onSave').and.callThrough();
const saveReportDatasetDetailsSpy = spyOn(service, 'saveReportDatasetDetails').and.callThrough();
const resetBaseDataSpy = spyOn(component, 'resetBaseData').and.callThrough();
const openSnackBarSpy = spyOn(component, 'openSnackBar').and.callThrough();
component.onSave();
expect(onSaveSpy).toHaveBeenCalledTimes(1);
expect(saveReportDatasetDetailsSpy).toHaveBeenCalledTimes(1);
// Since the response is undefined, it should not call resetBaseData and openSnackBar
expect(resetBaseDataSpy).toHaveBeenCalledTimes(0);
expect(openSnackBarSpy).toHaveBeenCalledTimes(0);
});
});