diff --git a/t/lib/NGCP/TestFramework.pm b/t/lib/NGCP/TestFramework.pm
new file mode 100644
index 0000000000..61d440a6d5
--- /dev/null
+++ b/t/lib/NGCP/TestFramework.pm
@@ -0,0 +1,180 @@
+package NGCP::TestFramework;
+
+use strict;
+use warnings;
+use Cpanel::JSON::XS;
+use Data::Walk;
+use DateTime qw();
+use DateTime::Format::Strptime qw();
+use DateTime::Format::ISO8601 qw();
+use Digest::MD5 qw/md5_hex/;
+use Moose;
+use Net::Domain qw(hostfqdn);
+use Test::More;
+use URI;
+use YAML::XS qw(LoadFile);
+use Data::Dumper;
+
+use NGCP::TestFramework::RequestBuilder;
+use NGCP::TestFramework::Client;
+use NGCP::TestFramework::TestExecutor;
+
+has 'file_path' => (
+    isa => 'Str',
+    is => 'ro'
+);
+
+sub run {
+    my ( $self ) = @_;
+
+    unless ( $self->file_path ) {
+        return;
+    }
+    $YAML::XS::DumpCode = 1;
+    $YAML::XS::LoadCode = 1;
+    $YAML::XS::UseCode = 1;
+    $YAML::XS::LoadBlessed = 1;
+
+    my $testing_data = LoadFile($self->file_path);
+
+    my $base_uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');
+    my $request_builder = NGCP::TestFramework::RequestBuilder->new({ base_uri => $base_uri });
+    my $client = NGCP::TestFramework::Client->new( { uri => $base_uri, log_debug => 0 } );
+    my $test_executor = NGCP::TestFramework::TestExecutor->new();
+
+    # initializing time to add to fields which need to be unique
+    my $retained = { unique_id => int(rand(100000)) };
+
+    foreach my $test ( @$testing_data ) {
+        next if ( $test->{skip} );
+
+        # build request
+        my $request = $request_builder->build({
+            method  => $test->{method},
+            path    => $test->{path},
+            header  => $test->{header} || undef,
+            content => $test->{content} || undef,
+            retain  => $retained
+        });
+
+        # handle separate types
+        if ( $test->{type} eq 'item' ) {
+            my $result = $client->perform_request($request);
+            if ( $test->{retain} ) {
+                $self->_get_retained_elements( $test->{retain}, $retained, $result );
+            }
+            if ( $test->{operations} ) {
+                $self->_perform_operations( $test->{operations}, $retained );
+            }
+            if ( $test->{perl_code} ){
+                my $sub = $test->{perl_code};
+                warn Dumper $sub;
+                $sub->( $retained );
+            }
+            $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} );
+        }
+        elsif ( $test->{type} eq 'batch' ) {
+            foreach my $iteration ( 1..$test->{iterations} ) {
+                my $result = $client->perform_request($request);
+                if ( $test->{retain} ) {
+                    $self->_get_retained_elements( $test->{retain}, $retained, $result );
+                }
+                if ( $test->{operations} ) {
+                    $self->_perform_operations( $test->{operations}, $retained );
+                }
+                $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} );
+            }
+        }
+        elsif ( $test->{type} eq 'pagination' ) {
+            my $nexturi = $test->{path};
+            do {
+                $request->uri( $base_uri.$nexturi );
+                my $result = $client->perform_request($request);
+                if ( $test->{retain} ) {
+                    $self->_get_retained_elements( $test->{retain}, $retained, $result );
+                }
+                if ( $test->{operations} ) {
+                    $self->_perform_operations( $test->{operations}, $retained );
+                }
+                my $body = decode_json( $result->decoded_content() );
+
+                #build default conditions for pagination
+                $test->{conditions}->{is}->{$nexturi} = $body->{_links}->{self}->{href};
+
+                my $colluri = URI->new($base_uri.$body->{_links}->{self}->{href});
+                my %q = $colluri->query_form;
+                $test->{conditions}->{ok}->{$q{page}} = 'defined';
+                $test->{conditions}->{ok}->{$q{rows}} = 'defined';
+                my $page = int($q{page});
+                my $rows = int($q{rows});
+                if($page == 1) {
+                    $test->{conditions}->{ok}->{'${collection}._links.prev.href'} = 'undefined';
+                } else {
+                    $test->{conditions}->{ok}->{'${collection}._links.prev.href'} = 'defined';
+                }
+                if(($retained->{collection}->{total_count} / $rows) <= $page) {
+                    $test->{conditions}->{ok}->{'${collection}._links.next.href'} = 'undefined';
+                } else {
+                    $test->{conditions}->{ok}->{'${collection}._links.next.href'} = 'defined';
+                }
+
+                $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} );
+
+                if( $body->{_links}->{next}->{href} ) {
+                    $nexturi = $body->{_links}->{next}->{href};
+                } else {
+                    $nexturi = undef;
+                }
+            } while ( $nexturi )
+        }
+    }
+}
+
+sub _get_retained_elements {
+    my ( $self, $retain, $retained, $result ) = @_;
+
+    while ( my ( $retain_elem, $retain_value ) = each %{$retain} ) {
+        if ( $retain_value =~ /.+\..+/ ) {
+            my @splitted_values = split (/\./, $retain_value);
+            $retained->{$retain_elem} = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retain_elem );
+        }
+        elsif ( $retain_value eq 'body' ) {
+            $retained->{$retain_elem} = decode_json( $result->decoded_content() );
+        }
+        else {
+            return {
+                success => 0,
+                message => 'Wrong retain instructions!'
+            }
+        }
+    }
+}
+
+sub _retrieve_from_composed_key {
+    my ( $self, $result, $splitted_values, $retain_elem ) = @_;
+
+    if ( $splitted_values->[0] eq 'header' ) {
+        my $value = $result->header(ucfirst $splitted_values->[1]);
+        if ( $retain_elem =~ /^.+_id$/ ) {
+            $value =~ /^.+\/(\d+)$/;
+            $value = $1;
+        }
+        return $value;
+    }
+}
+
+sub _variables_available {
+    my( $self, $retained, $test ) = @_;
+
+    return 0 if ( $test->{path} =~ /\$\{(.*)\}/ && !$retained->{$1} );
+
+    # substitute variables in content
+    if ( $test->{content} && ref $test->{content} eq 'HASH' ) {
+        foreach my $content_key (keys %{$test->{content}}) {
+            return 0 if ( $test->{content}->{$content_key} =~ /\$\{(.*)\}/ && !$retained->{$1} );
+        }
+    }
+    return 1;
+}
+
+1;
diff --git a/t/lib/NGCP/TestFramework/Client.pm b/t/lib/NGCP/TestFramework/Client.pm
new file mode 100644
index 0000000000..c4d7ede393
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/Client.pm
@@ -0,0 +1,191 @@
+package NGCP::TestFramework::Client;
+
+use strict;
+use warnings;
+use Data::Dumper;
+use Digest::MD5 qw/md5_hex/;
+use IO::Uncompress::Unzip;
+use LWP::UserAgent;
+use Moose;
+use Time::HiRes qw/gettimeofday tv_interval/;
+
+has 'username' => (
+    isa => 'Str',
+    is => 'ro',
+    default => sub {
+        $ENV{API_USER} // 'administrator';
+    }
+);
+
+has 'password' => (
+    isa => 'Str',
+    is => 'ro',
+    default => sub {
+        $ENV{API_PASS} // 'administrator';
+    }
+);
+
+has 'role' => (
+    isa => 'Str',
+    is => 'ro',
+    default => 'admin',
+);
+
+has 'uri' => (
+    isa => 'Str',
+    is => 'ro',
+    default => sub { $ENV{CATALYST_SERVER}; },
+);
+
+has 'sub_uri' => (
+    isa => 'Str',
+    is => 'ro',
+    default => sub { $ENV{CATALYST_SERVER_SUB}; },
+);
+
+has '_uri' => (
+    isa => 'Maybe[Str]',
+    is => 'rw',
+);
+
+has 'verify_ssl' => (
+    isa => 'Int',
+    is => 'ro',
+    default => 0,
+);
+
+has 'log_debug' => (
+    isa => 'Bool',
+    is => 'rw',
+    default => 0,
+);
+
+has '_crt_path' => (
+    is => 'ro',
+    isa => 'Str',
+    lazy => 1,
+    default => sub {
+        my ($self) = @_;
+        return '/tmp/' . md5_hex($self->username) . ".crt";
+    }
+);
+
+has 'last_rtt' => (
+    is => 'rw',
+    isa => 'Num',
+    default => 0,
+);
+
+has '_ua' => (
+    isa => 'LWP::UserAgent',
+    is => 'ro',
+    lazy => 1,
+    builder => '_build_ua'
+);
+
+sub _build_ua {
+    my $self = shift;
+
+    my $ua = LWP::UserAgent->new(keep_alive => 1);
+    my $uri;
+    if($self->role eq "admin" || $self->role eq "reseller") {
+        $uri = $self->uri;
+    } else {
+        $uri = $self->sub_uri;
+    }
+    $self->_uri($uri);
+    $self->debug("client using uri $uri\n");
+    $uri =~ s/^https?:\/\///;
+    $self->debug("client using ip:port $uri\n");
+    my $realm;
+    if($self->role eq 'admin' || $self->role eq 'reseller') {
+        $realm = 'api_admin_http';
+    } elsif($self->role eq 'subscriber') {
+        $realm = 'api_subscriber_http';
+    }
+    $self->debug("client using realm $realm with user=" . $self->username . " and pass " . $self->password . "\n");
+    $ua->credentials($uri, $realm, $self->username, $self->password);
+    unless($self->verify_ssl) {
+        $ua->ssl_opts(
+            verify_hostname => 0,
+            SSL_verify_mode => 0,
+        );
+    }
+    if($self->role eq "admin" || $self->role eq "reseller") {
+        unless(-f $self->_crt_path) {
+
+            # we have to setup a new connection here, because if we're already connected,
+            # the connection will be re-used, thus no cert is used
+            my $tmpua = LWP::UserAgent->new;
+            $tmpua->credentials($uri, $realm, $self->username, $self->password);
+            unless($self->verify_ssl) {
+                $tmpua->ssl_opts(
+                    verify_hostname => 0,
+                    SSL_verify_mode => 0,
+                );
+            }
+
+            my $res = $tmpua->post(
+                $self->_uri . '/api/admincerts/',
+                Content_Type => 'application/json',
+                Content => '{}'
+            );
+            unless($res->is_success) {
+                die "Failed to fetch client certificate: " . $res->status_line . "\n";
+            }
+            my $zip = $res->decoded_content;
+            my $z = IO::Uncompress::Unzip->new(\$zip, MultiStream => 0, Append => 1);
+            my $data;
+            while(!$z->eof() && (my $hdr = $z->getHeaderInfo())) {
+                unless($hdr->{Name} =~ /\.pem$/) {
+                    # wrong file, just read stream, clear buffer and try next
+                    while($z->read($data) > 0) {}
+                    $data = undef;
+                    $z->nextStream();
+                    next;
+                }
+                while($z->read($data) > 0) {}
+                last;
+            }
+            $z->close();
+            unless($data) {
+                die "Failed to find PEM file in client certificate zip file\n";
+            }
+            open my $fh, ">:raw", $self->_crt_path
+                or die "Failed to open " . $self->_crt_path . ": $!\n";
+            print $fh $data;
+            close $fh;
+        }
+        $ua->ssl_opts(
+            SSL_cert_file => $self->_crt_path,
+            SSL_key_file => $self->_crt_path,
+        );
+    }
+    return $ua;
+}
+
+
+sub perform_request {
+    my ($self, $req) = @_;
+    my $t0 = [gettimeofday];
+    $self->debug("content of " . $req->method . " request to " . $req->uri . ":\n");
+    $self->debug($req->content || "<empty request>");
+    $self->debug("\n");
+    my $res = $self->_ua->request($req);
+    my $rtt = tv_interval($t0);
+    $self->last_rtt($rtt);
+    $self->debug("content of response:\n");
+    $self->debug($res->decoded_content || "<empty response>");
+    $self->debug("\n");
+    return $res;
+}
+
+sub debug {
+    my ($self, $msg) = @_;
+    if($self->log_debug) {
+        print $msg;
+    }
+}
+
+1;
+
diff --git a/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml b/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml
new file mode 100644
index 0000000000..86121e455e
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml
@@ -0,0 +1,655 @@
+---
+#check options
+-
+    name: check OPTIONS for billingnetworks
+    type: item
+    method: OPTIONS
+    path: /api/billingnetworks/
+    conditions:
+        is:
+            code: 200
+            header:
+                Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-billingnetworks
+        ok:
+            options:
+                - GET
+                - HEAD
+                - OPTIONS
+                - POST
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{blocks} = [{ip=>'fdfe::5a55:caff:fefa:9089',mask=>128},
+                                   {ip=>'fdfe::5a55:caff:fefa:908a'},
+                                   {ip=>'fdfe::5a55:caff:fefa:908b',mask=>128},];
+        }
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test billing network ${unique_id}
+        description: test billing network description ${unique_id}
+        reseller_id: 1
+        blocks: ${blocks}
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork: body
+    conditions:
+        is:
+            code: 200
+
+#PUT test billingnetwork
+-
+    name: PUT test billingnetwork
+    type: item
+    method: PUT
+    path: '/${billingnetwork_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test billingnetwork PUT ${unique_id}
+        description: test billing network description PUT ${unique_id}
+        reseller_id: 1
+        blocks: ${blocks}
+    conditions:
+        is:
+            code: 200
+
+#GET billingnetwork
+-
+    name: fetch PUT test billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork: body
+    conditions:
+        is:
+            code: 200
+
+#PATCH test billingnetwork
+-
+    name: PATCH test billingnetwork
+    type: item
+    method: PATCH
+    path: '/${billingnetwork_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /name
+            value: test billingnetwork PATCH ${unique_id}
+    conditions:
+        is:
+            code: 200
+
+#GET billingnetwork
+-
+    name: fetch PATCHed test billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork: body
+    conditions:
+        is:
+            code: 200
+
+#DELETE billingnetwork
+-
+    name: terminate test billingnetwork
+    type: item
+    method: DELETE
+    path: '${billingnetwork_path}'
+    conditions:
+        is:
+            code: 204
+
+#GET billingnetwork
+-
+    name: try to fetch terminated billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork: body
+    conditions:
+        is:
+            code: 404
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv6 billing network 1 ${unique_id}
+        description: test ipv6 billing network description 1 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: 'fdfe::5a55:caff:fefa:9089'
+                mask: 128
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv6_1: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv6_1}->{id};
+            delete $retained->{billingnetwork_ipv6_1}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv6_1}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv6 billing network 2 ${unique_id}
+        description: test ipv6 billing network description 2 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: 'fdfe::5a55:caff:fefa:908a'
+                mask: null
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv6_2: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv6_2}->{id};
+            delete $retained->{billingnetwork_ipv6_2}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv6_2}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv6 billing network 3 ${unique_id}
+        description: test ipv6 billing network description 3 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: 'fdfe::5a55:caff:fefa:908b'
+                mask: 128
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv6_3: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv6_3}->{id};
+            delete $retained->{billingnetwork_ipv6_3}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv6_3}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv6 billing network 4 ${unique_id}
+        description: test ipv6 billing network description 4 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: 'fdfe::5a55:caff:fefa:9089'
+                mask: 128
+            -
+                ip: 'fdfe::5a55:caff:fefa:908a'
+                mask: null
+            -
+                ip: 'fdfe::5a55:caff:fefa:908b'
+                mask: 128
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv6_4: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv6_4}->{id};
+            delete $retained->{billingnetwork_ipv6_4}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv6_4}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv4 billing network 1 ${unique_id}
+        description: test ipv4 billing network description 1 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.4.7'
+                mask: 26
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv4_1: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv4_1}->{id};
+            delete $retained->{billingnetwork_ipv4_1}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv4_1}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv4 billing network 2 ${unique_id}
+        description: test ipv4 billing network description 2 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.4.99'
+                mask: 26
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv4_2: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv4_2}->{id};
+            delete $retained->{billingnetwork_ipv4_2}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv4_2}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv4 billing network 3 ${unique_id}
+        description: test ipv4 billing network description 3 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.5.9'
+                mask: 24
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv4_3: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv4_3}->{id};
+            delete $retained->{billingnetwork_ipv4_3}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv4_3}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv4 billing network 4 ${unique_id}
+        description: test ipv4 billing network description 4 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.6.9'
+                mask: 24
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv4_4: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv4_4}->{id};
+            delete $retained->{billingnetwork_ipv4_4}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv4_4}': *content
+
+#POST test billingnetwork
+-
+    name: POST test billingnetwork
+    type: item
+    method: POST
+    path: /api/billingnetworks/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: &content
+        name: test ipv4 billing network 5 ${unique_id}
+        description: test ipv4 billing network description 5 ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.4.7'
+                mask: 26
+            -
+                ip: '10.0.4.99'
+                mask: 26
+            -
+                ip: '10.0.5.9'
+                mask: 24
+            -
+                ip: '10.0.6.9'
+                mask: 24
+        status: active
+    retain:
+        billingnetwork_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#GET billingnetwork
+-
+    name: fetch POSTed billingnetwork
+    type: item
+    method: GET
+    path: '/${billingnetwork_path}'
+    retain:
+        billingnetwork_ipv4_5: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            delete $retained->{billingnetwork_ipv4_5}->{id};
+            delete $retained->{billingnetwork_ipv4_5}->{_links};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${billingnetwork_ipv4_5}': *content
+
+#compare filtered collection
+-
+    name: compare filtered collection
+    type: item
+    method: GET
+    path: '/api/billingnetworks/?page=1&rows=5&ip=fdfe::5a55:caff:fefa:9089&name=%25${unique_id}'
+    retain:
+        collection: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{expected} = [ $retained->{billingnetwork_ipv6_1}, $retained->{billingnetwork_ipv6_4} ];
+            map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+            map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${collection}._embedded.ngcp:billingnetworks': ${expected}
+
+#compare filtered collection
+-
+    name: compare filtered collection
+    type: item
+    method: GET
+    path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.4.0&name=%25${unique_id}'
+    retain:
+        collection: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{expected} = [ $retained->{billingnetwork_ipv4_1}, $retained->{billingnetwork_ipv4_5} ];
+            map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+            map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${collection}._embedded.ngcp:billingnetworks': ${expected}
+
+#compare filtered collection
+-
+    name: compare filtered collection
+    type: item
+    method: GET
+    path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.4.64&name=%25${unique_id}'
+    retain:
+        collection: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{expected} = [ $retained->{billingnetwork_ipv4_2}, $retained->{billingnetwork_ipv4_5} ];
+            map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+            map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${collection}._embedded.ngcp:billingnetworks': ${expected}
+
+#compare filtered collection
+-
+    name: compare filtered collection
+    type: item
+    method: GET
+    path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.5.255&name=%25${unique_id}'
+    retain:
+        collection: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{expected} = [ $retained->{billingnetwork_ipv4_3}, $retained->{billingnetwork_ipv4_5} ];
+            map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+            map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${collection}._embedded.ngcp:billingnetworks': ${expected}
+
+#compare filtered collection
+-
+    name: compare filtered collection
+    type: item
+    method: GET
+    path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.6.255&name=%25${unique_id}'
+    retain:
+        collection: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{expected} = [ $retained->{billingnetwork_ipv4_4}, $retained->{billingnetwork_ipv4_5} ];
+            map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+            map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}};
+        } 
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${collection}._embedded.ngcp:billingnetworks': ${expected}
\ No newline at end of file
diff --git a/t/lib/NGCP/TestFramework/Interface/Calls.yaml b/t/lib/NGCP/TestFramework/Interface/Calls.yaml
new file mode 100644
index 0000000000..52bc5f7569
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/Interface/Calls.yaml
@@ -0,0 +1,35 @@
+---
+#get a subscriber for testing
+-
+    name: get a subscriber for testing
+    type: item
+    method: GET
+    path: '/api/subscribers/?page=1&rows=1&order_by=id&order_by_direction=desc'
+    retain:
+        subscriber: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            my $subscriber = $retained->{subscriber}->{'_embedded'}->{'ngcp:subscribers'}->[0];
+            $retained->{subscriber} = $subscriber;
+            $retained->{subscriber_id} = $subscriber->{id};
+        } 
+    conditions:
+        is:
+            code: 200
+        ok:
+            '${subscriber}.id': defined
+
+#fetch calls for subscriber
+-
+    name: get a subscriber for testing
+    type: item
+    method: GET
+    path: '/api/calls/?page=1&rows=10&subscriber_id=${subscriber_id}'
+    retain:
+        calls: body
+    conditions:
+        is:
+            code: 200
+        ok:
+            '${calls}.total_count': defined
\ No newline at end of file
diff --git a/t/lib/NGCP/TestFramework/Interface/Contracts.yaml b/t/lib/NGCP/TestFramework/Interface/Contracts.yaml
new file mode 100644
index 0000000000..e7d0c01f8d
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/Interface/Contracts.yaml
@@ -0,0 +1,1015 @@
+---
+#check options
+-
+    name: check OPTIONS for contracts
+    type: item
+    method: OPTIONS
+    path: /api/contracts/
+    conditions:
+        is:
+            code: 200
+            header:
+                Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-contracts
+        ok:
+            options:
+                - GET
+                - HEAD
+                - OPTIONS
+                - POST
+
+#create a BillingProfile
+-
+    name: create a BillingProfile
+    type: item
+    thread: 1
+    method: POST
+    path: /api/billingprofiles/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test profile ${unique_id}
+        handle: test_profile_handle${unique_id}
+        reseller_id: 1
+    conditions:
+        is:
+            code: 201
+    retain:
+        billing_profile_id: header.location
+
+#create a Customer Contact
+-
+    name: create a Customer Contact
+    type: item
+    method: POST
+    path: /api/customercontacts/
+    header:
+        Content-Type: application/json
+    content:
+        firstname: cust_contact_first
+        lastname: cust_contact_last
+        email: cust_contact@custcontact.invalid
+        reseller_id: 1
+    conditions:
+        is:
+            code: 201
+    retain:
+        customer_contact_path: header.location
+        customer_contact_id: header.location
+
+#check CustomerContact
+-
+    name: check CustomerContact
+    type: item
+    method: GET
+    path: '/${customer_contact_path}'
+    conditions:
+        is:
+            code: 200
+
+#create a System Contact
+-
+    name: create a System Contact
+    type: item
+    method: POST
+    path: /api/systemcontacts/
+    header:
+        Content-Type: application/json
+    content:
+        firstname: sys_contact_first
+        lastname: sys_contact_last
+        email: sys_contact@syscontact.invalid
+    conditions:
+        is:
+            code: 201
+    retain:
+        system_contact_path: header.location
+        system_contact_id: header.location
+
+#get System Contact
+-
+    name: get System Contact
+    type: item
+    method: GET
+    path: '/${system_contact_path}'
+    retain:
+        system_contact: body
+
+#create batch
+-
+    name: create batch
+    type: batch
+    method: POST
+    path: '/api/contracts/'
+    iterations: 6
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        billing_profile_id: ${billing_profile_id}
+    retain:
+        contract_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#create invalid Contract with wrong type
+-
+    name: create invalid Contract with wrong type
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        billing_profile_id: ${billing_profile_id}
+        type: invalid
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: Validation failed.*type
+
+#create invalid Contract with wrong billing profile
+-
+    name: create invalid Contract with wrong billing profile
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        billing_profile_id: 999999
+        type: reseller
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: Invalid 'billing_profile_id'
+
+#create invalid Contract with customercontact
+-
+    name: create invalid Contract with customercontact
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        type: reseller
+        billing_profile_id: ${billing_profile_id}
+        contact_id: ${customer_contact_id}
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: The contact_id is not a valid ngcp:systemcontacts item
+
+#create invalid Contract without contact
+-
+    name: create invalid Contract without contact
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        type: reseller
+        billing_profile_id: ${billing_profile_id}
+    conditions:
+        is:
+            code: 422
+
+#create invalid Contract with invalid status
+-
+    name: create invalid Contract with invalid status
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: invalid
+        type: reseller
+        billing_profile_id: ${billing_profile_id}
+        contact_id: ${system_contact_id}
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: field='status'
+
+#verify pagination
+-
+    name: verify pagination
+    skip: 1
+    type: pagination
+    method: GET
+    path: '/api/contracts/?page=1&rows=5'
+    retain:
+        collection: body
+    conditions:
+        is:
+            code: 200
+
+#check options on contract
+-
+    name: check OPTIONS on contract
+    type: item
+    method: OPTIONS
+    path: '/${contract_path}'
+    conditions:
+        is:
+            code: 200
+        ok:
+            options:
+                - GET
+                - HEAD
+                - OPTIONS
+                - PUT
+                - PATCH
+
+#get contract
+-
+    name: GET contract
+    type: item
+    method: GET
+    path: '/${contract_path}'
+    retain:
+        contract: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{contract}->{all_billing_profiles}};
+        }
+    conditions:
+        is:
+            code: 200
+        ok:
+            '${contract}.status': defined
+            '${contract}.type': defined
+            '${contract}.all_billing_profiles': defined
+        like:
+            '${contract}.billing_profile_id': '[0-9]+'
+            '${contract}.contact_id': '[0-9]+'
+            '${contract}.id': '[0-9]+'
+        is_deeply:
+            '${contract}.all_billing_profiles':
+                -
+                    profile_id: ${billing_profile_id}
+                    start: null
+                    stop: null
+
+#put contract with missing content-type
+-
+    name: PUT contract with missing content-type
+    type: item
+    method: PUT
+    path: '/${contract_path}'
+    header:
+        Prefer: return=minimal
+    conditions:
+        is:
+            code: 415
+
+#put contract with unsupported content type
+-
+    name: PUT contract with unsupported Content-Type
+    type: item
+    method: PUT
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/xxx
+    conditions:
+        is:
+            code: 415
+
+#put contract with missing body
+-
+    name: PUT contract with missing body
+    type: item
+    method: PUT
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    conditions:
+        is:
+            code: 400
+
+#put contract
+-
+    name: PUT contract
+    type: item
+    method: PUT
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: '${contract}'
+    retain:
+        new_contract: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{new_contract}->{all_billing_profiles}};
+        }
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${contract}': ${new_contract}
+        ok:
+            '${new_contract}._links.ngcp:systemcontacts': defined
+            '${new_contract}._links.ngcp:billingprofiles': defined
+
+#modify contract status
+-
+    name: modify contract status
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: pending
+    retain:
+        modified_contract: body
+    conditions:
+        is: 
+            code: 200
+            '${modified_contract}.status': pending
+            '${modified_contract}._links.self.href': ${contract_path}
+            '${modified_contract}._links.collection.href': /api/contracts/
+
+#check patch with status undef
+-
+    name: check patch with status undef
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: null
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid status
+-
+    name: check patch with invalid status
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: invalid
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid contact_id
+-
+    name: check patch with invalid contact_id
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /contact_id
+            value: 99999
+    conditions:
+        is: 
+            code: 422
+
+#check patch with customer contact_id
+-
+    name: check patch with customer contact_id
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /contact_id
+            value: ${customer_contact_id}
+    conditions:
+        is: 
+            code: 422
+
+#check patch with undef billing_profile_id
+-
+    name: check patch with undef billing_profile_id
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: null
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid billing_profile_id
+-
+    name: check patch with invalid billing_profile_id
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: 99999
+    conditions:
+        is: 
+            code: 422
+
+#multi-bill-prof: create another test billing profile
+-
+    name: 'multi-bill-prof: create another test billing profile'
+    type: item
+    method: POST
+    path: '/api/billingprofiles/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: SECOND test profile ${unique_id}
+        handle: second_testprofile${unique_id}
+        reseller_id: 1
+    retain:
+        second_billing_profile_id: header.location
+    conditions:
+        is: 
+            code: 201
+    perl_code: !!perl/code |
+        {
+            my ( $retained ) = @_;
+
+            my $dtf = DateTime::Format::Strptime->new(
+                pattern => '%F %T',
+            ); #DateTime::Format::Strptime->new( pattern => '%Y-%m-%d %H:%M:%S' );
+            my $now = DateTime->now(
+                time_zone => DateTime::TimeZone->new(name => 'local')
+            );
+            my $t1 = $now->clone->add(days => 1);
+            my $t2 = $now->clone->add(days => 2);
+            my $t3 = $now->clone->add(days => 3);
+
+            my $billing_profile_id = $retained->{billing_profile_id};
+
+            $retained->{malformed_profilemappings1} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($now),
+                                                          stop => $dtf->format_datetime($now),} ];
+            $retained->{malformed_profilemappings2} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($t1),
+                                                          stop => $dtf->format_datetime($t1),} ];
+            $retained->{malformed_profilemappings3} = [ { profile_id => $billing_profile_id,
+                                                          start => undef,
+                                                          stop => $dtf->format_datetime($now),},];
+            $retained->{malformed_profilemappings4} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($t1),
+                                                          stop => $dtf->format_datetime($t2),}, ] , [];
+            $retained->{correct_profile_mappings1} = [ { profile_id => $retained->{second_billing_profile_id},
+                                                        start => undef,
+                                                        stop => undef, },
+                                                      { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t1),
+                                                        stop => $dtf->format_datetime($t2), },
+                                                       { profile_id => $billing_profile_id,
+                                                         start => $dtf->format_datetime($t2),
+                                                         stop => $dtf->format_datetime($t3), }
+                                                    ];
+            $retained->{correct_profile_mappings2} = [ { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t1),
+                                                        stop => $dtf->format_datetime($t2), },
+                                                      { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t2),
+                                                        stop => $dtf->format_datetime($t3), },
+                                                       { profile_id => $retained->{second_billing_profile_id},
+                                                         start => $dtf->format_datetime($t3),
+                                                         stop => undef, }
+                                                    ];
+        }
+
+#multi-bill-prof POST: check 'start' timestamp is not in future
+-
+    name: 'multi-bill-prof POST: check "start" timestamp is not in future'
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings1}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check 'start' timestamp has to be before 'stop' timestamp
+-
+    name: 'multi-bill-prof POST: check "start" timestamp has to be before "stop" timestamp'
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings2}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check Interval with 'stop' timestamp but no 'start' timestamp specified
+-
+    name: 'multi-bill-prof POST: check "Interval with "stop" timestamp but no "start" timestamp specified"'
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings3}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check An initial interval without 'start' and 'stop' timestamps is required
+-
+    name: 'multi-bill-prof POST: check An initial interval without "start" and "stop" timestamps is required'
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings4}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof: create test contract
+-
+    name: 'multi-bill-prof: create test contract'
+    type: item
+    method: POST
+    path: '/api/contracts/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${correct_profile_mappings1}
+    retain:
+        contract_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#get contract
+-
+    name: GET contract
+    type: item
+    method: GET
+    path: '/${contract_path}'
+    retain:
+        contract: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{contract}->{all_billing_profiles}};
+            $retained->{malformed_profilemappings4} = [ { profile_id => $retained->{billing_profile_id},
+                                                          start => undef,
+                                                          stop => undef,}, ];
+        }
+    conditions:
+        is:
+            code: 200
+            '${contract}.billing_profile_id': ${second_billing_profile_id}
+        ok:
+            '${contract}.profile_package_id': undefined
+            '${contract}.billing_profile_id': defined
+            '${contract}.billing_profiles': defined
+            '${contract}.all_billing_profiles': defined
+        is_deeply:
+            '${contract}.all_billing_profiles': ${correct_profile_mappings1}
+
+#multi-bill-prof PATCH: check 'start' timestamp is not in future
+-
+    name: 'multi-bill-prof PATCH: check "start" timestamp is not in future'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings1}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check 'start' timestamp has to be before 'stop' timestamp
+-
+    name: 'multi-bill-prof PATCH: check "start" timestamp has to be before "stop" timestamp'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings2}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check Interval with 'stop' timestamp but no 'start' timestamp specified
+-
+    name: 'multi-bill-prof PATCH: check Interval with "stop" timestamp but no "start" timestamp specified'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings3}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check Adding intervals without 'start' and 'stop' timestamps is not allowed.
+-
+    name: 'multi-bill-prof PATCH: check Adding intervals without "start" and "stop" timestamps is not allowed.'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings4}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: test if patching profile_package_id fails
+-
+    name: 'multi-bill-prof PATCH: test if patching profile_package_id fails'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /profile_package_id
+            value: null
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: test contract with new billing profile
+-
+    name: 'multi-bill-prof PATCH: test contract with new billing profile'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    retain:
+        patched_contract: body
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: ${billing_profile_id}
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+
+            $retained->{posted_profiles_number} = scalar @{$retained->{correct_profile_mappings1}} + 1;
+            $retained->{actual_profiles_number} = scalar @{$retained->{patched_contract}->{all_billing_profiles}};
+
+            my $now = DateTime->now(
+                time_zone => DateTime::TimeZone->new(name => 'local')
+            );
+            foreach my $m ( @{$retained->{patched_contract}->{billing_profiles}} ) {
+                if (!defined $m->{start}) {
+                    push(@{$retained->{expected_mappings}},$m);
+                    next;
+                }
+                my $s = $m->{start};
+                $s =~ s/^(\d{4}\-\d{2}\-\d{2})\s+(\d.+)$/$1T$2/;
+                my $start = DateTime::Format::ISO8601->parse_datetime($s);
+                $start->set_time_zone( DateTime::TimeZone->new(name => 'local') );
+                push(@{$retained->{expected_mappings}},$m) if ($start <= $now);
+            }
+            push ( @{$retained->{expected_mappings}}, @{$retained->{correct_profile_mappings2}} );
+        }
+    conditions:
+        is:
+            code: 200
+            '${patched_contract}.billing_profile_id': ${billing_profile_id}
+            '${posted_profiles_number}': '${actual_profiles_number}'
+        ok:
+            '${patched_contract}.profile_package_id': undefined
+            '${patched_contract}.billing_profile_id': defined
+            '${patched_contract}.billing_profiles': defined
+            '${patched_contract}.all_billing_profiles': defined
+
+#multi-bill-prof: patch test contract
+-
+    name: 'multi-bill-prof: patch test contract'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    retain:
+        patched_contract: body
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${correct_profile_mappings2}
+    conditions:
+        is:
+            code: 200
+
+#get contract
+-
+    name: GET contract
+    type: item
+    method: GET
+    path: '/${contract_path}'
+    retain:
+        got_contract: body
+    conditions:
+        is:
+            code: 200
+            '${patched_contract}.billing_profile_id': ${billing_profile_id}
+        ok:
+            '${patched_contract}.billing_profile_id': defined
+            '${patched_contract}.billing_profiles': defined
+        is_deeply:
+            '${got_contract}': '${patched_contract}'
+
+#multi-bill-prof: put test contract
+-
+    name: 'multi-bill-prof: put test contract'
+    type: item
+    method: PUT
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    retain:
+        updated_contract: body
+    content:
+        status: active
+        contact_id: ${system_contact_id}
+        type: reseller
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${correct_profile_mappings2}
+    conditions:
+        is:
+            code: 200
+
+#get contract
+-
+    name: GET contract
+    type: item
+    method: GET
+    path: '/${contract_path}'
+    retain:
+        got_contract: body
+    conditions:
+        is:
+            code: 200
+            '${updated_contract}.billing_profile_id': ${billing_profile_id}
+        ok:
+            '${updated_contract}.billing_profile_id': defined
+            '${updated_contract}.billing_profiles': defined
+        is_deeply:
+            '${got_contract}': '${updated_contract}'
+
+
+#try to delete contact before terminating contracts
+-
+    name: try to delete contact before terminating contracts
+    type: item
+    method: DELETE
+    path: '/api/systemcontacts/${system_contact_id}'
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{patched_contract}->{billing_profiles}};
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{updated_contract}->{billing_profiles}};
+        }
+    conditions:
+        is:
+            code: 423
+        is_deeply:
+            #perform tests against stripped mapping here, because deleting start_time would have affected previous is_deeply verification
+            '${patched_contract}.billing_profiles': '${expected_mappings}'
+            '${updated_contract}.billing_profiles': '${expected_mappings}'
+
+#multi-bill-prof: terminate contract
+-
+    name: 'multi-bill-prof: terminate contract'
+    type: item
+    method: PATCH
+    path: '/${contract_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        - 
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#try to get already terminated contract
+-
+    name: try to get already terminated contract
+    type: item
+    method: GET
+    path: '/${contract_path}'
+    conditions:
+        is:
+            code: 404
+
+#terminate billingprofile
+-
+    name: 'terminate billingprofile'
+    type: item
+    method: PATCH
+    path: '/api/billingprofiles/${billing_profile_id}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#get contract 1
+-
+    name: 'get contract 1'
+    type: item
+    method: GET
+    path: '/api/contracts/1'
+    retain:
+        contract1: body
+    conditions:
+        is:
+            code: 200
+            '${contract1}.id': 1
+            '${contract1}.type': reseller
+
+#check contract 1 can't be terminated
+-
+    name: 'check contract 1 can not be terminated'
+    type: item
+    method: PATCH
+    path: '/api/contracts/1'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 403
+
+#get contract 1 again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits
+-
+    name: 'get contract 1 again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits'
+    type: item
+    method: GET
+    path: '/api/contracts/1'
+    retain:
+        contract1: body
+    conditions:
+        is:
+            code: 200
+            '${contract1}.id': 1
+            '${contract1}.type': reseller
+
+#check contract 1 can't be terminated again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits
+-
+    name: 'check contract 1 can not be terminated again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits'
+    type: item
+    method: PATCH
+    path: '/api/contracts/1'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 403
diff --git a/t/lib/NGCP/TestFramework/Interface/Customers.yaml b/t/lib/NGCP/TestFramework/Interface/Customers.yaml
new file mode 100644
index 0000000000..e647fe6d73
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/Interface/Customers.yaml
@@ -0,0 +1,1374 @@
+---
+#check options
+-
+    name: check OPTIONS for customers
+    type: item
+    method: OPTIONS
+    path: /api/customers/
+    conditions:
+        is:
+            code: 200
+            header:
+                Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-customers
+        ok:
+            options:
+                - GET
+                - HEAD
+                - OPTIONS
+                - POST
+
+#create a BillingProfile
+-
+    name: create a BillingProfile
+    type: item
+    thread: 1
+    method: POST
+    path: /api/billingprofiles/
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test profile ${unique_id}
+        handle: test_profile_handle${unique_id}
+        reseller_id: 1
+    conditions:
+        is:
+            code: 201
+    retain:
+        billing_profile_id: header.location
+
+#create a Customer Contact
+-
+    name: create a Customer Contact
+    type: item
+    method: POST
+    path: /api/customercontacts/
+    header:
+        Content-Type: application/json
+    content:
+        firstname: cust_contact_first
+        lastname: cust_contact_last
+        email: cust_contact@custcontact.invalid
+        reseller_id: 1
+    conditions:
+        is:
+            code: 201
+    retain:
+        customer_contact_path: header.location
+        customer_contact_id: header.location
+
+#check CustomerContact
+-
+    name: check CustomerContact
+    type: item
+    method: GET
+    path: '/${customer_contact_path}'
+    conditions:
+        is:
+            code: 200
+
+#create a System Contact
+-
+    name: create a System Contact
+    type: item
+    method: POST
+    path: /api/systemcontacts/
+    header:
+        Content-Type: application/json
+    content:
+        firstname: sys_contact_first
+        lastname: sys_contact_last
+        email: sys_contact@syscontact.invalid
+    conditions:
+        is:
+            code: 201
+    retain:
+        system_contact_path: header.location
+        system_contact_id: header.location
+
+#get System Contact
+-
+    name: get System Contact
+    type: item
+    method: GET
+    path: '/${system_contact_path}'
+    retain:
+        system_contact: body
+
+#create batch
+-
+    name: create batch
+    type: batch
+    method: POST
+    path: '/api/customers/'
+    iterations: 6
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        billing_profile_id: ${billing_profile_id}
+        max_subscribers: null
+        external_id: null
+    retain:
+        customer_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#create invalid Customer with wrong type
+-
+    name: create invalid Customer with wrong type
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        billing_profile_id: ${billing_profile_id}
+        type: invalid
+        max_subscribers: null
+        external_id: null
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: is not a valid value
+
+#create invalid Customer with wrong billing profile
+-
+    name: create invalid Customer with wrong billing profile
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        billing_profile_id: 999999
+        type: sipaccount
+        max_subscribers: null
+        external_id: null
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: Invalid 'billing_profile_id'
+
+#create invalid Customer with systemcontact
+-
+    name: create invalid Customer with systemcontact
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        type: sipaccount
+        billing_profile_id: ${billing_profile_id}
+        contact_id: ${system_contact_id}
+        max_subscribers: null
+        external_id: null
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: The contact_id is not a valid ngcp:customercontacts item
+
+#create invalid Customer without contact
+-
+    name: create invalid Customer without contact
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        type: sipaccount
+        billing_profile_id: ${billing_profile_id}
+        max_subscribers: null
+        external_id: null
+    conditions:
+        is:
+            code: 422
+
+#create invalid Customer with invalid status
+-
+    name: create invalid Customer with invalid status
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: invalid
+        type: sipaccount
+        billing_profile_id: ${billing_profile_id}
+        contact_id: ${customer_contact_id}
+        max_subscribers: null
+        external_id: null
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: field='status'
+
+#create invalid Customer with invalid max_subscribers
+-
+    name: create invalid Customer with invalid status
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        type: sipaccount
+        billing_profile_id: ${billing_profile_id}
+        contact_id: ${customer_contact_id}
+        max_subscribers: 'abc'
+        external_id: null
+    conditions:
+        is:
+            code: 422
+            body.code: 422
+        like:
+            body.message: field='max_subscribers'
+
+#verify pagination
+-
+    skip: 1
+    name: verify pagination
+    type: pagination
+    method: GET
+    path: '/api/customers/?page=1&rows=5'
+    retain:
+        collection: body
+    conditions:
+        is:
+            code: 200
+
+#check options on customer
+-
+    name: check OPTIONS on customer
+    type: item
+    method: OPTIONS
+    path: '/${customer_path}'
+    conditions:
+        is:
+            code: 200
+        ok:
+            options:
+                - GET
+                - HEAD
+                - OPTIONS
+                - PUT
+                - PATCH
+
+#get customer
+-
+    name: GET customer
+    type: item
+    method: GET
+    path: '/${customer_path}'
+    retain:
+        customer: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{customer}->{all_billing_profiles}};
+        }
+    conditions:
+        is:
+            code: 200
+        ok:
+            '${customer}.status': defined
+            '${customer}.type': defined
+            '${customer}.billing_profile_id': defined
+            '${customer}.contact_id': defined
+            '${customer}.id': defined
+            '${customer}.all_billing_profiles': defined
+            '${customer}.product_id': undefined
+        like:
+            '${customer}.billing_profile_id': '[0-9]+'
+            '${customer}.contact_id': '[0-9]+'
+            '${customer}.id': '[0-9]+'
+        is_deeply:
+            '${customer}.all_billing_profiles':
+                -
+                    profile_id: ${billing_profile_id}
+                    start: null
+                    stop: null
+                    network_id: null
+
+#put customer with missing content-type
+-
+    name: PUT customer with missing content-type
+    type: item
+    method: PUT
+    path: '/${customer_path}'
+    header:
+        Prefer: return=minimal
+    conditions:
+        is:
+            code: 415
+
+#put customer with unsupported content type
+-
+    name: PUT customer with unsupported Content-Type
+    type: item
+    method: PUT
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/xxx
+    conditions:
+        is:
+            code: 415
+
+#put customer with missing body
+-
+    name: PUT customer with missing body
+    type: item
+    method: PUT
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    conditions:
+        is:
+            code: 400
+
+#put customer
+-
+    name: PUT customer
+    type: item
+    method: PUT
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content: '${customer}'
+    retain:
+        new_customer: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{new_customer}->{all_billing_profiles}};
+            delete $retained->{customer}->{modify_timestamp};
+            delete $retained->{new_customer}->{modify_timestamp};
+        }
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            '${customer}': ${new_customer}
+        ok:
+            '${new_customer}._links.ngcp:customercontacts': defined
+            '${new_customer}._links.ngcp:billingprofiles': defined
+
+#modify customer status
+-
+    name: modify customer status
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: pending
+    retain:
+        modified_customer: body
+    conditions:
+        is: 
+            code: 200
+            '${modified_customer}.status': pending
+            '${modified_customer}._links.self.href': ${customer_path}
+            '${modified_customer}._links.collection.href': /api/customers/
+
+#check patch with status undef
+-
+    name: check patch with status undef
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: null
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid status
+-
+    name: check patch with invalid status
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: invalid
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid contact_id
+-
+    name: check patch with invalid contact_id
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /contact_id
+            value: 99999
+    conditions:
+        is: 
+            code: 422
+
+#check patch with system contact_id
+-
+    name: check patch with system contact_id
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /contact_id
+            value: ${system_contact_id}
+    conditions:
+        is: 
+            code: 422
+
+#check patch with undef billing_profile_id
+-
+    name: check patch with undef billing_profile_id
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: null
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid billing_profile_id
+-
+    name: check patch with invalid billing_profile_id
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: 99999
+    conditions:
+        is: 
+            code: 422
+
+#check patch with invalid max_subscribers
+-
+    name: check patch with invalid billing_profile_id
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /max_subscribers
+            value: 'abc'
+    conditions:
+        is: 
+            code: 422
+
+#multi-bill-prof: create another test billing profile
+-
+    name: 'multi-bill-prof: create another test billing profile'
+    type: item
+    method: POST
+    path: '/api/billingprofiles/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: SECOND test profile ${unique_id}
+        handle: second_testprofile${unique_id}
+        reseller_id: 1
+    retain:
+        second_billing_profile_id: header.location
+    conditions:
+        is: 
+            code: 201
+
+#multi-bill-prof: create test billing network
+-
+    name: 'multi-bill-prof: create test billing network'
+    type: item
+    method: POST
+    path: '/api/billingnetworks/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test billing network ${unique_id}
+        description: test billing network description ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.4.7'
+                mask: '26'
+            -
+                ip: '10.0.4.99'
+                mask: '26'
+            -
+                ip: '10.0.5.9'
+                mask: '24'
+            -
+                ip: '10.0.6.9'
+                mask: '24'
+    retain:
+        billing_network_id: header.location
+    conditions:
+        is: 
+            code: 201
+    perl_code: !!perl/code |
+        {
+            my ( $retained ) = @_;
+
+            my $dtf = DateTime::Format::Strptime->new(
+                pattern => '%F %T',
+            ); #DateTime::Format::Strptime->new( pattern => '%Y-%m-%d %H:%M:%S' );
+            my $now = DateTime->now(
+                time_zone => DateTime::TimeZone->new(name => 'local')
+            );
+            my $t1 = $now->clone->add(days => 1);
+            my $t2 = $now->clone->add(days => 2);
+            my $t3 = $now->clone->add(days => 3);
+
+            my $billing_profile_id = $retained->{billing_profile_id};
+
+            $retained->{malformed_profilemappings1} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($now),
+                                                          stop => $dtf->format_datetime($now),} ];
+            $retained->{malformed_profilemappings2} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($t1),
+                                                          stop => $dtf->format_datetime($t1),} ];
+            $retained->{malformed_profilemappings3} = [ { profile_id => $billing_profile_id,
+                                                          start => undef,
+                                                          stop => $dtf->format_datetime($now),},];
+            $retained->{malformed_profilemappings4} = [ { profile_id => $billing_profile_id,
+                                                          start => $dtf->format_datetime($t1),
+                                                          stop => $dtf->format_datetime($t2),}, ] , [];
+            $retained->{correct_profile_mappings1} = [ { profile_id => $retained->{second_billing_profile_id},
+                                                        start => undef,
+                                                        stop => undef,
+                                                        network_id => undef },
+                                                      { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t1),
+                                                        stop => $dtf->format_datetime($t2),
+                                                        network_id => undef },
+                                                       { profile_id => $billing_profile_id,
+                                                         start => $dtf->format_datetime($t2),
+                                                         stop => $dtf->format_datetime($t3),
+                                                         network_id => undef }
+                                                    ];
+            $retained->{correct_profile_mappings2} = [ { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t1),
+                                                        stop => $dtf->format_datetime($t2),
+                                                        network_id=> undef },
+                                                      { profile_id => $billing_profile_id,
+                                                        start => $dtf->format_datetime($t2),
+                                                        stop => $dtf->format_datetime($t3),
+                                                        network_id => undef },
+                                                       { profile_id => $retained->{second_billing_profile_id},
+                                                         start => $dtf->format_datetime($t3),
+                                                         stop => undef,
+                                                         network_id => $retained->{billing_network_id} }
+                                                    ];
+        }
+
+#multi-bill-prof POST: check 'start' timestamp is not in future
+-
+    name: 'multi-bill-prof POST: check "start" timestamp is not in future'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings1}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check 'start' timestamp has to be before 'stop' timestamp
+-
+    name: 'multi-bill-prof POST: check "start" timestamp has to be before "stop" timestamp'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings2}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check Interval with 'stop' timestamp but no 'start' timestamp specified
+-
+    name: 'multi-bill-prof POST: check "Interval with "stop" timestamp but no "start" timestamp specified"'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings3}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof POST: check An initial interval without 'start' and 'stop' timestamps is required
+-
+    name: 'multi-bill-prof POST: check An initial interval without "start" and "stop" timestamps is required'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${malformed_profilemappings4}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof: create test customer
+-
+    name: 'multi-bill-prof: create test customer'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${correct_profile_mappings1}
+    retain:
+        customer_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#get customer
+-
+    name: GET customer
+    type: item
+    method: GET
+    path: '/${customer_path}'
+    retain:
+        customer: body
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{customer}->{all_billing_profiles}};
+            $retained->{malformed_profilemappings4} = [ { profile_id => $retained->{billing_profile_id},
+                                                          start => undef,
+                                                          stop => undef,}, ];
+        }
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${second_billing_profile_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.billing_profiles': defined
+            '${customer}.all_billing_profiles': defined
+        is_deeply:
+            '${customer}.all_billing_profiles': ${correct_profile_mappings1}
+
+#multi-bill-prof PATCH: check 'start' timestamp is not in future
+-
+    name: 'multi-bill-prof PATCH: check "start" timestamp is not in future'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings1}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check 'start' timestamp has to be before 'stop' timestamp
+-
+    name: 'multi-bill-prof PATCH: check "start" timestamp has to be before "stop" timestamp'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings2}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check Interval with 'stop' timestamp but no 'start' timestamp specified
+-
+    name: 'multi-bill-prof PATCH: check Interval with "stop" timestamp but no "start" timestamp specified'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings3}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: check Adding intervals without 'start' and 'stop' timestamps is not allowed.
+-
+    name: 'multi-bill-prof PATCH: check Adding intervals without "start" and "stop" timestamps is not allowed.'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${malformed_profilemappings4}
+    conditions:
+        is:
+            code: 422
+
+#multi-bill-prof PATCH: test customer with new billing profile
+-
+    name: 'multi-bill-prof PATCH: test customer with new billing profile'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    retain:
+        patched_customer: body
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: ${billing_profile_id}
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+
+            $retained->{posted_profiles_number} = scalar @{$retained->{correct_profile_mappings1}} + 1;
+            $retained->{actual_profiles_number} = scalar @{$retained->{patched_customer}->{all_billing_profiles}};
+
+            my $now = DateTime->now(
+                time_zone => DateTime::TimeZone->new(name => 'local')
+            );
+            foreach my $m ( @{$retained->{patched_customer}->{billing_profiles}} ) {
+                if (!defined $m->{start}) {
+                    push(@{$retained->{expected_mappings}},$m);
+                    next;
+                }
+                my $s = $m->{start};
+                $s =~ s/^(\d{4}\-\d{2}\-\d{2})\s+(\d.+)$/$1T$2/;
+                my $start = DateTime::Format::ISO8601->parse_datetime($s);
+                $start->set_time_zone( DateTime::TimeZone->new(name => 'local') );
+                push(@{$retained->{expected_mappings}},$m) if ($start <= $now);
+            }
+            push ( @{$retained->{expected_mappings}}, @{$retained->{correct_profile_mappings2}} );
+        }
+    conditions:
+        is:
+            code: 200
+            '${patched_customer}.billing_profile_id': ${billing_profile_id}
+            '${posted_profiles_number}': '${actual_profiles_number}'
+        ok:
+            '${patched_customer}.billing_profile_id': defined
+            '${patched_customer}.billing_profiles': defined
+            '${patched_customer}.all_billing_profiles': defined
+
+#multi-bill-prof: patch test customer
+-
+    name: 'multi-bill-prof: patch test customer'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    retain:
+        patched_customer: body
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${correct_profile_mappings2}
+    conditions:
+        is:
+            code: 200
+
+#get customer
+-
+    name: GET customer
+    type: item
+    method: GET
+    path: '/${customer_path}'
+    retain:
+        got_customer: body
+    conditions:
+        is:
+            code: 200
+            '${patched_customer}.billing_profile_id': ${billing_profile_id}
+        ok:
+            '${patched_customer}.billing_profile_id': defined
+            '${patched_customer}.billing_profiles': defined
+        is_deeply:
+            '${got_customer}': '${patched_customer}'
+
+#multi-bill-prof: put test customer
+-
+    name: 'multi-bill-prof: put test customer'
+    type: item
+    method: PUT
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    retain:
+        updated_customer: body
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        max_subscriber: null
+        external_id: null
+        billing_profile_definition: profiles
+        billing_profiles: ${correct_profile_mappings2}
+    conditions:
+        is:
+            code: 200
+
+#get customer
+-
+    name: GET customer
+    type: item
+    method: GET
+    path: '/${customer_path}'
+    retain:
+        got_customer: body
+    conditions:
+        is:
+            code: 200
+            '${updated_customer}.billing_profile_id': ${billing_profile_id}
+        ok:
+            '${updated_customer}.billing_profile_id': defined
+            '${updated_customer}.billing_profiles': defined
+        is_deeply:
+            '${got_customer}': '${updated_customer}'
+
+
+#multi-bill-prof: terminate customer
+-
+    name: 'multi-bill-prof: terminate customer'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{patched_customer}->{billing_profiles}};
+            map { delete $_->{effective_start_time}; $_; } @{$retained->{updated_customer}->{billing_profiles}};
+        }
+    conditions:
+        is:
+            code: 200
+        is_deeply:
+            #perform tests against stripped mapping here, because deleting start_time would have affected previous is_deeply verification
+            '${patched_customer}.billing_profiles': '${expected_mappings}'
+            '${updated_customer}.billing_profiles': '${expected_mappings}'
+
+#terminate billingprofile
+-
+    name: 'terminate billingprofile'
+    type: item
+    method: PATCH
+    path: '/api/billingprofiles/${second_billing_profile_id}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#terminate billingnetwork
+-
+    name: 'terminate billingnetwork'
+    type: item
+    method: PATCH
+    path: '/api/billingnetworks/${billing_network_id}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#prof-package: create another test billing profile
+-
+    name: 'prof-package: create another test billing profile'
+    type: item
+    method: POST
+    path: '/api/billingprofiles/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: THIRD test profile ${unique_id}
+        handle: third_testprofile${unique_id}
+        reseller_id: 1
+    retain:
+        third_billing_profile_id: header.location
+    conditions:
+        is: 
+            code: 201
+
+#prof-package: create test billingnetwork
+-
+    name: 'prof-package: create test billingnetwork'
+    type: item
+    method: POST
+    path: '/api/billingnetworks/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: another test billing network ${unique_id}
+        description: another test billing network description ${unique_id}
+        reseller_id: 1
+        blocks:
+            -
+                ip: '10.0.4.7'
+                mask: '26'
+            -
+                ip: '10.0.4.99'
+                mask: '26'
+            -
+                ip: '10.0.5.9'
+                mask: '24'
+            -
+                ip: '10.0.6.9'
+                mask: '24'
+    retain:
+        second_billing_network_id: header.location
+    conditions:
+        is: 
+            code: 201
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+            $retained->{initial_profiles} = [ { profile_id => $retained->{billing_profile_id} },
+                                              { 
+                                                profile_id => $retained->{third_billing_profile_id},
+                                                network_id => $retained->{second_billing_network_id}
+                                              }
+                                            ]
+        }
+
+#prof-package: create test profile package
+-
+    name: 'prof-package: create test profile package'
+    type: item
+    method: POST
+    path: '/api/profilepackages/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: test profile package ${unique_id}
+        description: test profile package description ${unique_id}
+        reseller_id: 1
+        initial_profiles: ${initial_profiles}
+    retain:
+        profile_package_id: header.location
+    conditions:
+        is: 
+            code: 201
+
+#prof-package: create second test profile package
+-
+    name: 'prof-package: create second test profile package'
+    type: item
+    method: POST
+    path: '/api/profilepackages/'
+    header:
+        Content-Type: application/json
+        Prefer: return=representation
+    content:
+        name: second test profile package ${unique_id}
+        description: second test profile package description ${unique_id}
+        reseller_id: 1
+        initial_profiles: ${initial_profiles}
+    retain:
+        second_profile_package_id: header.location
+    conditions:
+        is: 
+            code: 201
+
+#prof-package: create test customer
+-
+    name: 'prof-package: create test customer'
+    type: item
+    method: POST
+    path: '/api/customers/'
+    header:
+        Content-Type: application/json
+    content:
+        status: active
+        contact_id: ${customer_contact_id}
+        type: sipaccount
+        billing_profile_definition: package
+        max_subscribers: null
+        external_id: null
+        profile_package_id: ${profile_package_id}
+    retain:
+        customer_path: header.location
+    conditions:
+        is:
+            code: 201
+
+#prof-package: get test customer
+-
+    name: 'prof-package: get test customer'
+    type: item
+    method: GET
+    path: '${customer_path}'
+    header:
+        Content-Type: application/json
+    retain:
+        customer: body
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${third_billing_profile_id}
+            '${customer}.profile_package_id': ${profile_package_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.profile_package_id': defined
+    perl_code: !!perl/code |
+        {
+            my ($retained) = @_;
+
+            my $dtf = DateTime::Format::Strptime->new(
+                pattern => '%F %T',
+            );
+            my $now = DateTime->now(
+                time_zone => DateTime::TimeZone->new(name => 'local')
+            );
+            my $t1 = $now->clone->add(days => 1);
+            my $t2 = $now->clone->add(days => 2);
+            my $t3 = $now->clone->add(days => 3);
+
+            $retained->{billing_profiles} = [ { profile_id => $retained->{billing_profile_id},
+                                            start => $dtf->format_datetime($t1),
+                                            stop => $dtf->format_datetime($t2) } ,
+                                          { profile_id => $retained->{third_billing_profile_id},
+                                            network_id => $retained->{second_billing_network_id},
+                                            start => $dtf->format_datetime($t2),
+                                            stop => $dtf->format_datetime($t3) } ];
+        }
+
+#prof-package: patch test customer
+-
+    name: 'prof-package: patch test customer'
+    type: item
+    method: PATCH
+    path: '${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profiles
+            value: ${billing_profiles}
+    retain:
+        customer: body
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${third_billing_profile_id}
+            '${customer}.profile_package_id': ${profile_package_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.profile_package_id': defined
+
+#prof-package: patch test customer
+-
+    name: 'prof-package: patch test customer'
+    type: item
+    method: PATCH
+    path: '${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /billing_profile_id
+            value: ${billing_profile_id}
+    retain:
+        customer: body
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${billing_profile_id}
+            '${customer}.profile_package_id': ${profile_package_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.profile_package_id': defined
+
+#prof-package: patch test customer
+-
+    name: 'prof-package: patch test customer'
+    type: item
+    method: PATCH
+    path: '${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /profile_package_id
+            value: ${profile_package_id}
+    retain:
+        customer: body
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${billing_profile_id}
+            '${customer}.profile_package_id': ${profile_package_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.profile_package_id': defined
+
+#prof-package: patch test customer
+-
+    name: 'prof-package: patch test customer'
+    type: item
+    method: PATCH
+    path: '${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /profile_package_id
+            value: ${second_profile_package_id}
+    retain:
+        customer: body
+    conditions:
+        is:
+            code: 200
+            '${customer}.billing_profile_id': ${third_billing_profile_id}
+            '${customer}.profile_package_id': ${second_profile_package_id}
+        ok:
+            '${customer}.billing_profile_id': defined
+            '${customer}.profile_package_id': defined
+
+#prof-package: terminate customer
+-
+    name: 'prof-package: terminate customer'
+    type: item
+    method: PATCH
+    path: '/${customer_path}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#prof-package: delete profile package
+-
+    name: 'prof-package: delete profile package'
+    type: item
+    method: DELETE
+    path: '/api/profilepackages/${profile_package_id}'
+    conditions:
+        is:
+            code: 204
+
+#prof-package: delete second profile package
+-
+    name: 'prof-package: delete second profile package'
+    type: item
+    method: DELETE
+    path: '/api/profilepackages/${second_profile_package_id}'
+    conditions:
+        is:
+            code: 204
+
+#prof-package: terminate third billing profile
+-
+    name: 'prof-package: terminate third billing profile'
+    type: item
+    method: PATCH
+    path: '/api/billingprofiles/${third_billing_profile_id}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#prof-package: terminate second billing network
+-
+    name: 'prof-package: terminate second billing network'
+    type: item
+    method: PATCH
+    path: '/api/billingnetworks/${second_billing_network_id}'
+    header:
+        Content-Type: application/json-patch+json
+        Prefer: return=representation
+    content:
+        -
+            op: replace
+            path: /status
+            value: terminated
+    conditions:
+        is:
+            code: 200
+
+#check locked status for deleting used contact
+-
+    name: check locked status for deleting used contact
+    type: item
+    method: DELETE
+    path: '/${customer_contact_path}'
+    conditions:
+        is:
+            code: 423
diff --git a/t/lib/NGCP/TestFramework/RequestBuilder.pm b/t/lib/NGCP/TestFramework/RequestBuilder.pm
new file mode 100644
index 0000000000..df4eaf3aef
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/RequestBuilder.pm
@@ -0,0 +1,105 @@
+package NGCP::TestFramework::RequestBuilder;
+
+use strict;
+use warnings;
+use HTTP::Request;
+use Cpanel::JSON::XS;
+use Moose;
+use Data::Dumper;
+
+has 'base_uri' => (
+    isa => 'Str',
+    is => 'ro'
+);
+
+sub build {
+    my ( $self, $args ) = @_;
+
+    my @methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
+
+    if ( !$args->{method} && !grep { $_ eq $args->{method} } @methods ) {
+        return {
+            success => 0,
+            message => 'HTTP method missing or incorrect!'
+        };
+    }
+
+    if ( !$args->{path} ) {
+        return {
+            success => 0,
+            message => 'Path missing!'
+        };
+    }
+
+    $self->_replace_vars($args);
+
+    my $req = HTTP::Request->new( $args->{method}, $self->base_uri.$args->{path} );
+    grep { $req->header( $_ => $args->{header}->{$_} ) } keys %{$args->{header}} if $args->{header};
+    $req->content( encode_json( $args->{content} ) ) if $args->{content};
+
+    return $req;
+}
+
+sub _replace_vars {
+    my ( $self, $args ) = @_;
+
+    # substitute variables in path
+    if ( $args->{path} =~ /\$\{(.*)\}/ ) {
+        $args->{path} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/;
+    }
+
+    # substitute variables in content
+    if ( $args->{content} ) {
+        if ( ref $args->{content} eq 'HASH' ) {
+            foreach my $content_key (keys %{$args->{content}}) {
+                if ( $args->{content}->{$content_key} && $args->{content}->{$content_key} =~ /\$\{(.*)\}$/ ) {
+                    if ( ref $args->{retain}->{$1} eq 'ARRAY' || ref $args->{retain}->{$1} eq 'HASH' ) {
+                        $args->{content}->{$content_key} = $args->{retain}->{$1};
+                    }
+                    else {
+                        $args->{content}->{$content_key} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/;
+                    }
+                }
+                elsif ( $args->{content}->{$content_key} && $args->{content}->{$content_key} =~ /^\$\{(.*)\}\..+/ ) {
+                    my @splitted_values = split (/\./, $args->{content}->{$content_key});
+                    $args->{content}->{$content_key} = $self->_retrieve_from_composed_key( \@splitted_values, $args->{retain} );
+                }
+            }
+        }
+        elsif ( ref $args->{content} eq 'ARRAY' ) {
+            foreach my $content ( @{$args->{content}} ) {
+                foreach my $content_key (keys %$content) {
+                    if ( $content->{$content_key} && $content->{$content_key} =~ /\$\{(.*)\}$/ ) {
+                        if ( ref $args->{retain}->{$1} eq 'ARRAY' || ref $args->{retain}->{$1} eq 'HASH' ) {
+                            $content->{$content_key} = $args->{retain}->{$1};
+                        }
+                        else {
+                            $content->{$content_key} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/;
+                        }
+                    }
+                    elsif ( $content->{$content_key} && $content->{$content_key} =~ /^\$\{(.*)\}\..+/ ) {
+                        my @splitted_values = split (/\./, $content->{$content_key});
+                        $content->{$content_key} = $self->_retrieve_from_composed_key( \@splitted_values, $args->{retain} );
+                    }
+                }
+            }
+        }
+        else {
+            if ( $args->{content} =~ /\$\{(.*)\}/ ) {
+                $args->{content} = $args->{retain}->{$1};
+            }
+        }
+    }
+}
+
+sub _retrieve_from_composed_key {
+    my ( $self, $splitted_values, $retained ) = @_;
+
+    if ( $splitted_values->[0] =~ /\$\{(.*)\}/ ) {
+        my $value = $retained->{$1};
+        grep { $value = $value->{$splitted_values->[$_]} } (1..(scalar @$splitted_values - 1));
+        return $value;
+    }
+}
+
+1;
diff --git a/t/lib/NGCP/TestFramework/TestExecutor.pm b/t/lib/NGCP/TestFramework/TestExecutor.pm
new file mode 100644
index 0000000000..a0810b73ec
--- /dev/null
+++ b/t/lib/NGCP/TestFramework/TestExecutor.pm
@@ -0,0 +1,115 @@
+package NGCP::TestFramework::TestExecutor;
+
+use strict;
+use warnings;
+use Cpanel::JSON::XS;
+use Data::Walk;
+use Moose;
+use Test::More;
+use Data::Dumper;
+
+sub run_tests {
+    my ( $self, $conditions, $result, $retained, $test_name ) = @_;
+
+    foreach my $condition ( keys %$conditions ) {
+        if ( $condition eq 'is' ) {
+            while ( my ( $check_param, $check_value ) = each %{$conditions->{$condition}} ) {
+                if ( $check_value =~ /^\$\{(.*)\}$/ ) {
+                    $check_value = $retained->{$1};
+                }
+                if ( $check_param =~ /^\$\{(.*)\}$/ ) {
+                    $check_param = $retained->{$1};
+                }
+                if ( $check_param =~ /.+\..+/ ) {
+                    my @splitted_values = split (/\./, $check_param);
+                    $check_param = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained );
+                    is ($check_param, $check_value, $test_name);
+                }
+                elsif ( $check_param eq 'code' ) {
+                    is ($result->code, $check_value, $test_name);
+                }
+                elsif ( $check_param eq 'header' ) {
+                    foreach my $header_condition ( keys %{$conditions->{$condition}->{$check_param}} ) {
+                        is ($result->header($header_condition), $check_value->{$header_condition}, $test_name);
+                    }
+                }
+                else {
+                    is ($check_param, $check_value, $test_name);
+                }
+            }
+        }
+        elsif ( $condition eq 'ok' ) {
+            foreach my $check_param (keys %{$conditions->{$condition}}) {
+                if ( $check_param eq 'options' ) {
+                    my $body = decode_json($result->decoded_content);
+                    my @hopts = split /\s*,\s*/, $result->header('Allow');
+                    ok(exists $body->{methods} && ref $body->{methods} eq "ARRAY", $test_name);
+                    foreach my $opt(@{$conditions->{$condition}->{$check_param}}) {
+                        ok(grep { /^$opt$/ } @hopts, $test_name);
+                        ok(grep { /^$opt$/ } @{ $body->{methods} }, $test_name);
+                    }
+                }
+                if ( $conditions->{$condition}->{$check_param} eq 'defined' || $conditions->{$condition}->{$check_param} eq 'undefined') {
+                    if ( $check_param =~ /.+\..+/ ) {
+                        my @splitted_values = split (/\./, $check_param);
+                        my $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained );
+                        $conditions->{$condition}->{$check_param} eq 'defined' ?
+                            ok(defined $check_value, $test_name) : ok(!defined $check_value, $test_name);
+                    }
+                }
+            }
+        }
+        elsif ( $condition eq 'like' ) {
+            foreach my $check_param (keys %{$conditions->{$condition}}) {
+                if ( $check_param =~ /.+\..+/ ) {
+                    my @splitted_values = split (/\./, $check_param);
+                    my $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained );
+                    like ($check_value, qr/$conditions->{$condition}->{$check_param}/, $test_name);
+                }
+            }
+        }
+        elsif ( $condition eq 'is_deeply' ) {
+            foreach my $check_param (keys %{$conditions->{$condition}}) {
+                *replace_variables = sub {
+                    if ( ref $_ eq 'HASH' ) {
+                        while ( my ( $key, $value ) = each %$_ ) {
+                            if ( $value && $value =~ /\$\{(.*)\}/ ) {
+                                $_->{$key} = $retained->{$1};
+                            }
+                        }
+                    }
+                };
+
+                walkdepth {wanted => \&replace_variables}, $conditions->{$condition}->{$check_param};
+                if ( $conditions->{$condition}->{$check_param} =~ /^\$\{(.*)\}$/ ) {
+                    $conditions->{$condition}->{$check_param} = $retained->{$1};
+                }
+                my $check_value;
+                if ( $check_param=~ /^\$\{(.*)\}$/ ) {
+                    $check_value = $retained->{$1};
+                }
+                if ( $check_param =~ /.+\..+/ ) {
+                    my @splitted_values = split (/\./, $check_param);
+                    $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained );
+                }
+                is_deeply ($check_value, $conditions->{$condition}->{$check_param}, $test_name);
+            }
+        }
+    }
+}
+
+sub _retrieve_from_composed_key {
+    my ( $self, $result, $splitted_values, $retained ) = @_;
+
+    if ( $splitted_values->[0] eq 'body' ) {
+        my $body = decode_json($result->decoded_content);
+        return $body->{$splitted_values->[1]};
+    }
+    elsif ( $splitted_values->[0] =~ /\$\{(.*)\}/ ) {
+        my $value = $retained->{$1};
+        grep { $value = $value->{$splitted_values->[$_]} } (1..(scalar @$splitted_values - 1));
+        return $value;
+    }
+}
+
+1;
diff --git a/t/lib/NGCP/testrunner.pl b/t/lib/NGCP/testrunner.pl
new file mode 100644
index 0000000000..dda4626d3e
--- /dev/null
+++ b/t/lib/NGCP/testrunner.pl
@@ -0,0 +1,81 @@
+use lib '..';
+use strict;
+use warnings;
+use NGCP::TestFramework;
+use Test::More;
+use threads;
+use Thread::Queue;
+use Data::Dumper;
+
+my $server = $ARGV[0] || undef;
+my $selected = $ARGV[1] || 'all';
+
+if ( !$server ){
+    print "Usage: \$ perl testrunner.pl [<testsystem>] [<testset>]\n";
+    print "Usage example: \$ perl testrunner.pl 192.168.88.162\n";
+    print "Usage example: \$ perl testrunner.pl 192.168.88.162 fast\n";
+    print "Possible test set: all, stable, fast, t/lib/NGCP/TestFramework/Interface/Contracts.yaml\n";
+    print "Default test set: all\n";
+    exit(1);
+}
+
+my @test_files;
+
+if ( $selected eq 'stable' ) {
+    print "Test selection: stable\n";
+}
+elsif ( $selected eq 'fast' ) {
+    print "Test selection: fast\n";
+}
+elsif ( $selected eq 'all' ) {
+    print "Test selection: all\n";
+    map { push @test_files, "TestFramework/Interface/$_" } `ls TestFramework/Interface`;
+}
+else{
+    print "Test selection: $selected\n";
+    push @test_files, $selected;
+}
+
+print "################################################################################\n";
+print "Finished main setup, now running tests ...\n";
+
+$ENV{CATALYST_SERVER_SUB}="https://$server:443";
+$ENV{CATALYST_SERVER}="https://$server:1443";
+$ENV{NGCP_SESSION_ID}=int(rand(1000)).time;
+
+my @threads;
+my $tests_queue = Thread::Queue->new();
+
+for ( @test_files ) {
+    $tests_queue->enqueue($_);
+}
+
+$tests_queue->end();
+
+for (1..2) {
+    push @threads, threads->create( {'context' => 'void'}, \&worker, $tests_queue );
+}
+
+foreach ( @threads ){
+  $_->join();
+}
+
+done_testing();
+
+sub worker {
+    my ($tests_queue) = @_;
+
+    while ( my $test_file = $tests_queue->dequeue_nb() ) {
+        my $start_time = time;
+        print "Running tests from $test_file\n";
+        my $test_framework = NGCP::TestFramework->new( {file_path => $test_file} );
+
+        my $result_code = $test_framework->run();
+
+        my $total_time = time - $start_time;
+        print "Finished test execution for $test_file, test execution returned with exit code $result_code.\n";
+        print "Tests for $test_file took $total_time seconds.\n";
+    }
+}
+
+1;