diff --git a/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts b/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a1978b971248669d91b8c9fb424d467849451022
--- /dev/null
+++ b/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts
@@ -0,0 +1,88 @@
+import { TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+
+import { provideMockActions } from '@ngrx/effects/testing';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects';
+import { Observable } from 'rxjs';
+import { cold, hot } from 'jasmine-marbles';
+import { ToastrService } from 'ngx-toastr';
+
+import { AttributeDistinctEffects } from './attribute-distinct.effects';
+import { AttributeDistinctService } from '../services/attribute-distinct.service';
+import * as attributeDistinctActions from '../actions/attribute-distinct.actions';
+import * as datasetSelector from '../selectors/dataset.selector';
+import { ATTRIBUTE } from '../../../test-data';
+
+describe('[Metamodel][Effects] AttributeDistinctEffects', () => {
+    let actions = new Observable();
+    let effects: AttributeDistinctEffects;
+    let metadata: EffectsMetadata<AttributeDistinctEffects>;
+    let service: AttributeDistinctService;
+    let toastr: ToastrService;
+    let store: MockStore;
+    const initialState = { metamodel: { } };
+    let mockDatasetSelectorSelectDatasetNameByRoute;
+    let router: Router;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            providers: [
+                AttributeDistinctEffects,
+                { provide: AttributeDistinctService, useValue: { }},
+                { provide: Router, useValue: { navigate: jest.fn() }},
+                { provide: ToastrService, useValue: {
+                        success: jest.fn(),
+                        error: jest.fn()
+                    }},
+                provideMockActions(() => actions),
+                provideMockStore({ initialState })
+            ]
+        }).compileComponents();
+        effects = TestBed.inject(AttributeDistinctEffects);
+        metadata = getEffectsMetadata(effects);
+        service = TestBed.inject(AttributeDistinctService);
+        toastr = TestBed.inject(ToastrService);
+        router = TestBed.inject(Router);
+        store = TestBed.inject(MockStore);
+        mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
+            datasetSelector.selectDatasetNameByRoute,''
+        );
+    });
+
+    it('should be created', () => {
+        expect(effects).toBeTruthy();
+    });
+
+    describe('loadAttributeDistinct$ effect', () => {
+        it('should dispatch the loadAttributeDistinctListSuccess action on success', () => {
+            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
+                datasetSelector.selectDatasetNameByRoute, 'myDataset'
+            );
+
+            const action = attributeDistinctActions.loadAttributeDistinctList({ attribute: ATTRIBUTE });
+            const outcome = attributeDistinctActions.loadAttributeDistinctListSuccess({ values: [] });
+
+            actions = hot('-a', { a: action });
+            const response = cold('-a|', { a: [] });
+            const expected = cold('--b', { b: outcome });
+            service.retrieveAttributeDistinctList = jest.fn(() => response);
+
+            expect(effects.loadAttributeDistinct$).toBeObservable(expected);
+            expect(service.retrieveAttributeDistinctList).toHaveBeenCalledWith('myDataset', ATTRIBUTE);
+        });
+
+        it('should dispatch the loadAttributeDistinctListFail action on HTTP failure', () => {
+            const action = attributeDistinctActions.loadAttributeDistinctList({ attribute: ATTRIBUTE });
+            const error = new Error();
+            const outcome = attributeDistinctActions.loadAttributeDistinctListFail();
+
+            actions = hot('-a', { a: action });
+            const response = cold('-#|', { }, error);
+            const expected = cold('--b', { b: outcome });
+            service.retrieveAttributeDistinctList = jest.fn(() => response);
+
+            expect(effects.loadAttributeDistinct$).toBeObservable(expected);
+        });
+    });
+});
diff --git a/client/src/app/metamodel/effects/attribute-distinct.effects.ts b/client/src/app/metamodel/effects/attribute-distinct.effects.ts
index aaa51d7cdc83ba114efe06823526c7ae0395310c..6584c439e5ca4be3558606f0785413ab354a0860 100644
--- a/client/src/app/metamodel/effects/attribute-distinct.effects.ts
+++ b/client/src/app/metamodel/effects/attribute-distinct.effects.ts
@@ -17,10 +17,18 @@ import { map, mergeMap, catchError } from 'rxjs/operators';
 import * as attributeDistinctActions from '../actions/attribute-distinct.actions';
 import { AttributeDistinctService } from '../services/attribute-distinct.service';
 import * as datasetSelector from '../selectors/dataset.selector';
- 
+
+/**
+ * @class
+ * @classdesc Attribute distinct effects.
+ */
 @Injectable()
 export class AttributeDistinctEffects {
-    loadAttributeDistinct$ = createEffect(() =>
+
+    /**
+     * Calls action to retrieve distinct attribute list.
+     */
+    loadAttributeDistinct$ = createEffect((): any =>
         this.actions$.pipe(
             ofType(attributeDistinctActions.loadAttributeDistinctList),
             concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
@@ -32,7 +40,7 @@ export class AttributeDistinctEffects {
             )
         )
     );
- 
+
     constructor(
         private actions$: Actions,
         private attributeDistinctService: AttributeDistinctService,
diff --git a/client/src/app/metamodel/effects/attribute.effects.spec.ts b/client/src/app/metamodel/effects/attribute.effects.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..602d95d2b34db2e6ce7aaa4e75c2a53b065b8473
--- /dev/null
+++ b/client/src/app/metamodel/effects/attribute.effects.spec.ts
@@ -0,0 +1,289 @@
+import { TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+
+import { provideMockActions } from '@ngrx/effects/testing';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects';
+import { Observable } from 'rxjs';
+import { cold, hot } from 'jasmine-marbles';
+
+import { ToastrService } from 'ngx-toastr';
+import { AttributeEffects } from './attribute.effects';
+import { AttributeService } from '../services/attribute.service';
+import * as attributeActions from '../actions/attribute.actions';
+import { ATTRIBUTE, ATTRIBUTE_LIST } from '../../../test-data';
+import * as datasetSelector from '../selectors/dataset.selector';
+
+describe('[Metamodel][Effects] AttributeEffects', () => {
+    let actions = new Observable();
+    let effects: AttributeEffects;
+    let metadata: EffectsMetadata<AttributeEffects>;
+    let service: AttributeService;
+    let toastr: ToastrService;
+    let store: MockStore;
+    const initialState = { metamodel: { } };
+    let mockDatasetSelectorSelectDatasetNameByRoute;
+    let router: Router;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            providers: [
+                AttributeEffects,
+                { provide: AttributeService, useValue: { }},
+                { provide: Router, useValue: { navigate: jest.fn() }},
+                { provide: ToastrService, useValue: {
+                        success: jest.fn(),
+                        error: jest.fn()
+                    }},
+                provideMockActions(() => actions),
+                provideMockStore({ initialState })
+            ]
+        }).compileComponents();
+        effects = TestBed.inject(AttributeEffects);
+        metadata = getEffectsMetadata(effects);
+        service = TestBed.inject(AttributeService);
+        toastr = TestBed.inject(ToastrService);
+        store = TestBed.inject(MockStore);
+        mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
+            datasetSelector.selectDatasetNameByRoute,''
+        );
+        router = TestBed.inject(Router);
+    });
+
+    it('should be created', () => {
+        expect(effects).toBeTruthy();
+    });
+
+    describe('loadAttributes$ effect', () => {
+        it('should dispatch the loadAttributeListSuccess action on success', () => {
+            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
+                datasetSelector.selectDatasetNameByRoute, 'myDataset'
+            );
+
+            const action = attributeActions.loadAttributeList();
+            const outcome = attributeActions.loadAttributeListSuccess({ attributes: ATTRIBUTE_LIST });
+
+            actions = hot('-a', { a: action });
+            const response = cold('-a|', { a: ATTRIBUTE_LIST });
+            const expected = cold('--b', { b: outcome });
+            service.retrieveAttributeList = jest.fn(() => response);
+
+            expect(effects.loadAttributes$).toBeObservable(expected);
+            expect(service.retrieveAttributeList).toHaveBeenCalledWith('myDataset');
+        });
+
+        it('should dispatch the loadAttributeListFail action on HTTP failure', () => {
+            const action = attributeActions.loadAttributeList();
+            const error = new Error();
+            const outcome = attributeActions.loadAttributeListFail();
+
+            actions = hot('-a', { a: action });
+            const response = cold('-#|', { }, error);
+            const expected = cold('--b', { b: outcome });
+            service.retrieveAttributeList = jest.fn(() => response);
+
+            expect(effects.loadAttributes$).toBeObservable(expected);
+        });
+    });
+
+    describe('addAttribute$ effect', () => {
+        it('should dispatch the addAttributeSuccess action on success', () => {
+            mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
+                datasetSelector.selectDatasetNameByRoute, 'myDataset'
+            );
+
+            const action = attributeActions.addAttribute({ attribute: ATTRIBUTE });
+            const outcome = attributeActions.addAttributeSuccess({ attribute: ATTRIBUTE });
+
+            actions = hot('-a', { a: action });
+            const response = cold('-a|', { a: ATTRIBUTE });
+            const expected = cold('--b', { b: outcome });
+            service.addAttribute = jest.fn(() => response);
+
+            expect(effects.addAttribute$).toBeObservable(expected);
+            expect(service.addAttribute).toHaveBeenCalledWith('myDataset', ATTRIBUTE);
+        });
+
+        it('should dispatch the addAttributeFail action on HTTP failure', () => {
+            const action = attributeActions.addAttribute({ attribute: ATTRIBUTE });
+            const error = new Error();
+            const outcome = attributeActions.addAttributeFail();
+
+            actions = hot('-a', { a: action });
+            const response = cold('-#|', { }, error);
+            const expected = cold('--b', { b: outcome });
+            service.addAttribute = jest.fn(() => response);
+
+            expect(effects.addAttribute$).toBeObservable(expected);
+        });
+    });
+
+    describe('addAttributeSuccess$ effect', () => {
+        it('should not dispatch', () => {
+            expect(metadata.addAttributeSuccess$).toEqual(
+                expect.objectContaining({ dispatch: false })
+            );
+        });
+
+        it('should display a success notification', () => {
+            const toastrSpy = jest.spyOn(toastr, 'success');
+            const action = attributeActions.addAttributeSuccess({ attribute: ATTRIBUTE });
+
+            actions = hot('a', { a: action });
+            const expected = cold('a', { a: action });
+
+            expect(effects.addAttributeSuccess$).toBeObservable(expected);
+            expect(toastrSpy).toHaveBeenCalledTimes(1);
+            expect(toastrSpy).toHaveBeenCalledWith(
+                'Attribute successfully added',
+                'The new attribute was added into the database'
+            );
+        });
+    });
+
+    describe('addAttributeFail$ effect', () => {
+        it('should not dispatch', () => {
+            expect(metadata.addAttributeFail$).toEqual(
+                expect.objectContaining({ dispatch: false })
+            );
+        });
+
+        it('should display an error notification', () => {
+            const spy = jest.spyOn(toastr, 'error');
+            const action = attributeActions.addAttributeFail();
+
+            actions = hot('a', { a: action });
+            const expected = cold('a', { a: action });
+
+            expect(effects.addAttributeFail$).toBeObservable(expected);
+            expect(spy).toHaveBeenCalledTimes(1);
+            expect(spy).toHaveBeenCalledWith(
+                'Failure to add attribute',
+                'The new attribute could not be added into the database'
+            );
+        });
+    });
+
+    describe('editAttribute$ effect', () => {
+        it('should dispatch the editAttributeSuccess action on success', () => {
+            const action = attributeActions.editAttribute({ attribute: ATTRIBUTE });
+            const outcome = attributeActions.editAttributeSuccess({ attribute: ATTRIBUTE });
+
+            actions = hot('-a', { a: action });
+            const response = cold('-a|', { a: ATTRIBUTE });
+            const expected = cold('--b', { b: outcome });
+            service.editAttribute = jest.fn(() => response);
+
+            expect(effects.editAttribute$).toBeObservable(expected);
+        });
+
+        it('should dispatch the editAttributeFail action on HTTP failure', () => {
+            const action = attributeActions.editAttribute({ attribute: ATTRIBUTE });
+            const error = new Error();
+            const outcome = attributeActions.editAttributeFail();
+
+            actions = hot('-a', { a: action });
+            const response = cold('-#|', { }, error);
+            const expected = cold('--b', { b: outcome });
+            service.editAttribute = jest.fn(() => response);
+
+            expect(effects.editAttribute$).toBeObservable(expected);
+        });
+    });
+
+    describe('editAttributeFail$ effect', () => {
+        it('should not dispatch', () => {
+            expect(metadata.editAttributeFail$).toEqual(
+                expect.objectContaining({ dispatch: false })
+            );
+        });
+
+        it('should display an error notification', () => {
+            const spy = jest.spyOn(toastr, 'error');
+            const action = attributeActions.editAttributeFail();
+
+            actions = hot('a', { a: action });
+            const expected = cold('a', { a: action });
+
+            expect(effects.editAttributeFail$).toBeObservable(expected);
+            expect(spy).toHaveBeenCalledTimes(1);
+            expect(spy).toHaveBeenCalledWith(
+                'Failure to edit attribute',
+                'The existing attribute could not be edited into the database'
+            );
+        });
+    });
+
+    describe('deleteAttribute$ effect', () => {
+        it('should dispatch the deleteAttributeSuccess action on success', () => {
+            const action = attributeActions.deleteAttribute({ attribute: ATTRIBUTE });
+            const outcome = attributeActions.deleteAttributeSuccess({ attribute: ATTRIBUTE });
+
+            actions = hot('-a', { a: action });
+            const response = cold('-a|', { a: ATTRIBUTE });
+            const expected = cold('--b', { b: outcome });
+            service.deleteAttribute = jest.fn(() => response);
+
+            expect(effects.deleteAttribute$).toBeObservable(expected);
+        });
+
+        it('should dispatch the deleteAttributeFail action on HTTP failure', () => {
+            const action = attributeActions.deleteAttribute({ attribute: ATTRIBUTE });
+            const error = new Error();
+            const outcome = attributeActions.deleteAttributeFail();
+
+            actions = hot('-a', { a: action });
+            const response = cold('-#|', { }, error);
+            const expected = cold('--b', { b: outcome });
+            service.deleteAttribute = jest.fn(() => response);
+
+            expect(effects.deleteAttribute$).toBeObservable(expected);
+        });
+    });
+
+    describe('deleteAttributeSuccess$ effect', () => {
+        it('should not dispatch', () => {
+            expect(metadata.deleteAttributeSuccess$).toEqual(
+                expect.objectContaining({ dispatch: false })
+            );
+        });
+
+        it('should display a success notification', () => {
+            const toastrSpy = jest.spyOn(toastr, 'success');
+            const action = attributeActions.deleteAttributeSuccess({ attribute: ATTRIBUTE });
+
+            actions = hot('a', { a: action });
+            const expected = cold('a', { a: action });
+
+            expect(effects.deleteAttributeSuccess$).toBeObservable(expected);
+            expect(toastrSpy).toHaveBeenCalledTimes(1);
+            expect(toastrSpy).toHaveBeenCalledWith(
+                'Attribute successfully deleted',
+                'The existing attribute has been deleted'
+            );
+        });
+    });
+
+    describe('deleteAttributeFail$ effect', () => {
+        it('should not dispatch', () => {
+            expect(metadata.deleteAttributeFail$).toEqual(
+                expect.objectContaining({ dispatch: false })
+            );
+        });
+
+        it('should display an error notification', () => {
+            const spy = jest.spyOn(toastr, 'error');
+            const action = attributeActions.deleteAttributeFail();
+
+            actions = hot('a', { a: action });
+            const expected = cold('a', { a: action });
+
+            expect(effects.deleteAttributeFail$).toBeObservable(expected);
+            expect(spy).toHaveBeenCalledTimes(1);
+            expect(spy).toHaveBeenCalledWith(
+                'Failure to delete attribute',
+                'The existing attribute could not be deleted from the database'
+            );
+        });
+    });
+});
diff --git a/client/src/app/metamodel/effects/attribute.effects.ts b/client/src/app/metamodel/effects/attribute.effects.ts
index 59e6c66114e1ce62e7b1bac1aa3b0f24452b7d08..c1198e7edf545cad34ef6da44b992b6a21a4e961 100644
--- a/client/src/app/metamodel/effects/attribute.effects.ts
+++ b/client/src/app/metamodel/effects/attribute.effects.ts
@@ -19,9 +19,18 @@ import * as attributeActions from '../actions/attribute.actions';
 import { AttributeService } from '../services/attribute.service';
 import * as datasetSelector from '../selectors/dataset.selector';
 
+
+/**
+ * @class
+ * @classdesc Attribute effects.
+ */
 @Injectable()
 export class AttributeEffects {
-    loadAttributes$ = createEffect(() =>
+
+    /**
+     * Calls action to retrieve attribute list.
+     */
+    loadAttributes$ = createEffect((): any =>
         this.actions$.pipe(
             ofType(attributeActions.loadAttributeList),
             concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
@@ -34,7 +43,10 @@ export class AttributeEffects {
         )
     );
 
-    addAttribute$ = createEffect(() =>
+    /**
+     * Calls action to add an attribute.
+     */
+    addAttribute$ = createEffect((): any =>
         this.actions$.pipe(
             ofType(attributeActions.addAttribute),
             concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
@@ -47,6 +59,9 @@ export class AttributeEffects {
         )
     );
 
+    /**
+     * Displays add attribute success notification.
+     */
     addAttributeSuccess$ = createEffect(() =>
         this.actions$.pipe(
             ofType(attributeActions.addAttributeSuccess),
@@ -56,6 +71,9 @@ export class AttributeEffects {
         ), { dispatch: false }
     );
 
+    /**
+     * Displays add attribute error notification.
+     */
     addAttributeFail$ = createEffect(() =>
         this.actions$.pipe(
             ofType(attributeActions.addAttributeFail),
@@ -63,7 +81,10 @@ export class AttributeEffects {
         ), { dispatch: false }
     );
 
-    editAttribute$ = createEffect(() =>
+    /**
+     * Calls action to modify an attribute.
+     */
+    editAttribute$ = createEffect((): any =>
         this.actions$.pipe(
             ofType(attributeActions.editAttribute),
             concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
@@ -76,6 +97,9 @@ export class AttributeEffects {
         )
     );
 
+    /**
+     * Displays edit attribute error notification.
+     */
     editAttributeFail$ = createEffect(() =>
         this.actions$.pipe(
             ofType(attributeActions.editAttributeFail),
@@ -83,7 +107,10 @@ export class AttributeEffects {
         ), { dispatch: false }
     );
 
-    deleteAttribute$ = createEffect(() =>
+    /**
+     * Calls action to remove an attribute.
+     */
+    deleteAttribute$ = createEffect((): any =>
         this.actions$.pipe(
             ofType(attributeActions.deleteAttribute),
             concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)),
@@ -96,6 +123,9 @@ export class AttributeEffects {
         )
     );
 
+    /**
+     * Displays delete attribute success notification.
+     */
     deleteAttributeSuccess$ = createEffect(() =>
         this.actions$.pipe(
             ofType(attributeActions.deleteAttributeSuccess),
@@ -103,6 +133,9 @@ export class AttributeEffects {
         ), { dispatch: false }
     );
 
+    /**
+     * Displays delete attribute error notification.
+     */
     deleteAttributeFail$ = createEffect(() =>
         this.actions$.pipe(
             ofType(attributeActions.deleteAttributeFail),
diff --git a/client/src/app/metamodel/effects/column.effects.spec.ts b/client/src/app/metamodel/effects/column.effects.spec.ts
index ef28d818d0c4adcd08c1e4236eadeeef5ce242d4..64ef0d2deab1e4510daffd389974ebf68d60a954 100644
--- a/client/src/app/metamodel/effects/column.effects.spec.ts
+++ b/client/src/app/metamodel/effects/column.effects.spec.ts
@@ -54,7 +54,7 @@ describe('[Metamodel][Effects] ColumnEffects', () => {
         expect(effects).toBeTruthy();
     });
 
-    describe('loadOutputFamilies$ effect', () => {
+    describe('loadColumns$ effect', () => {
         it('should dispatch the loadColumnListSuccess action on success', () => {
             mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector(
                 datasetSelector.selectDatasetNameByRoute, 'myDataset'