I work on a project that contains a number of public facing WCF interfaces and recently we were doing a pre release to our test environment when we found out that our public contracts were not working, the interfaces were fine but some of the data members of the contracts were not being serialized. On inspecting the WSDL we realised that some of the namespaces had been changed, it turns out when a rename had been done on some of the namespaces and a shuffling around of some components to more logical namespaces.
The main issue is that we were using the default namespaces provided by the WCF API and not hand crafting our public facing namespaces to be more specific to the business but irrespective of that we didn’t have a way of knowing that we have potentially broken a public facing contract.
Solution
Breaking a public interface is not necessarily a bad thing but it is if you don’t know that you have done it. What we needed was a series of unit tests that could be run by our CI server and flag up errors when a contract is broken. This would allow the team to discuss the breaking change and determine if the change is required or if there is another way of implementing the change.
Given the following service and data contracts I will show how to create a series of tests that will break if the contract changes. No custom namespace has been applied so the default .net namespaces will be used but this same process can be used for service contracts with custom namespaces.
namespace Foo { [ServiceContract] public interface IPublicInterface { [OperationContract] GetInfoResponse GetInfo(GetInfoRequest request); } [DataContract] public class GetInfoRequest { [DataMember] public string RequestData { get; set; } } [DataContract] public class GetInfoResponse { public GetInfoResponse(string result { Result = result; } [DataMember] public string Result { get; private set; } } }
As you can see it is a very simple interface using simple request and response objects, each object has a single DataMember. Now assuming that this interface has been made public we now need to apply a safe guard that will warn us if the contract is broken. To do this we need to create a suite of tests that can run a legacy service contract at our real service contract. .Net makes this easy by allowing you to customize the namespace and name of the ServiceContract and DataContract attributes. Ultimately we are trying to create a client side contract that matches the currently live WSDL of our public service. The following tests make use of my WCF MockServiceHostFactory, NSubstitute mock framework and FluentAssertions testing assertion framework.
namespace Foo.Tests { [TestFixture] class When_calling_service_using_the_version_1_service_contract { private ServiceHost publicInterfaceServiceHost; private ChannelFactory<ILegacyPublicInterface_v1> legacyPublicInterfaceChannelFactory; private IPublicInterface publicInterfaceMock; private ILegacyPublicInterface_v1 publicInterfaceChannel; [SetUp] public void SetUp() { publicInterfaceMock = Substitute.For<IPublicInterface>(); publicInterfaceMock.GetInfo(Arg.Any<GetInfoRequest>()).Returns(new GetInfoResponse("Success", string.Empty)); publicInterfaceServiceHost = MockServiceHostFactory.GenerateMockServiceHost(publicInterfaceMock, new Uri("http://localhost:8123/PublicInterface")); publicInterfaceServiceHost.Open(); legacyPublicInterfaceChannelFactory = new ChannelFactory<ILegacyPublicInterface_v1>(new BasicHttpBinding(), new EndpointAddress("http://localhost:8123/PublicInterface")); publicInterfaceChannel = legacyPublicInterfaceChannelFactory.CreateChannel(); } [TearDown] public void TearDown() { legacyPublicInterfaceChannelFactory.Close(); publicInterfaceServiceHost.Close(); } [Test] public void Should_deserialize_request_object_at_the_service() { publicInterfaceChannel.GetInfo(new GetInfoRequest_v1 { RequestData = "Foo" }); publicInterfaceMock.Received().GetInfo(Arg.Is<GetInfoRequest>(x => x.RequestData == "Foo")); } [Test] public void Should_deserialize_the_response_object_at_the_client() { GetInfoResponse_v1 response = publicInterfaceChannel.GetInfo(new GetInfoRequest_v1()); response.ShouldHave().AllProperties().EqualTo(new GetInfoResponse_v1 { Result = "Success" }); } } [ServiceContract(Namespace = "http://tempuri.org/", Name = "IPublicInterface")] interface ILegacyPublicInterface_v1 { [OperationContract] GetInfoResponse_v1 GetInfo(GetInfoRequest_v1 request); } [DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Foo", Name = "GetInfoRequest")] class GetInfoRequest_v1 { [DataMember] public string RequestData { get; set; } } [DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Foo", Name = "GetInfoResponse")] class GetInfoResponse_v1 { [DataMember] public string Result { get; set; } } }
If we were now to move any of the components involved in the public contract to different namespaces, renames the interface or anything that would break the contract this test would fail indicating that we need to either revert the code, fix the contract by providing custom names and namespaces or inform our clients that there will be a breaking change in the next release.
Testing different contract versions
Not all contract changes have to break a contract. In some cases we might want to add an additional field to a request or response object or add a new operation to the contract. Given we need to add a new field called Message to the GetInfoResponse object the following test provides coverage for the new contract whilst still maintaining the test for the previous version of the contract.
[DataContract] public class GetInfoResponse { public GetInfoResponse(string result, string message) { //... Message = message; } //... [DataMember] public string Message { get; private set; } } [TestFixture] class When_calling_service_using_the_version_2_service_contract { //... private ChannelFactory<ILegacyPublicInterface_v2> legacyPublicInterfaceChannelFactory; private ILegacyPublicInterface_v2 publicInterfaceChannel; [SetUp] public void SetUp() { //... publicInterfaceMock.GetInfo(Arg.Any<GetInfoRequest>()).Returns(new GetInfoResponse("Success", "Bar")); legacyPublicInterfaceChannelFactory = new ChannelFactory<ILegacyPublicInterface_v2>(new BasicHttpBinding(), new EndpointAddress("http://localhost:8123/PublicInterface")); } [Test] public void Should_deserialize_the_response_object_at_the_client() { GetInfoResponse_v2 response = publicInterfaceChannel.GetInfo(new GetInfoRequest_v1()); response.ShouldHave().AllProperties().EqualTo(new GetInfoResponse_v2 { Result = "Success", Message = "Bar" }); } } [ServiceContract(Namespace = "http://tempuri.org/", Name = "IPublicInterface")] interface ILegacyPublicInterface_v2 { [OperationContract] GetInfoResponse_v2 GetInfo(GetInfoRequest_v1 request); } [DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Foo", Name = "GetInfoResponse")] class GetInfoResponse_v2 { [DataMember] public string Result { get; set; } [DataMember] public string Message { get; set; } }
We now have a version two of the public interface and response object which can be used to test the contract as in the previous test but it has left the original legacy contract untouched meaning that the first version of the contract will still be tested for compatibility.
Summary
It is very important that you are happy with your public interface before you give it to your clients as once it is in use it is very difficult to change and the more clients the bigger the impact. To minimize the chance of change the contract via tools such as ReSharper you should provide custom namespaces for all of your contract components and in my opinion it looks much more professional to a consuming third party to see your companies name as the root URL in the WSDL.
And finally these tests will not protect you from everything but they do provide a safety.
What these tests do:
- Check for breaking changes in your public contracts at the WCF layer
- Allow you to test legacy versions of the contracts
What these tests don’t do:
- Force your real services to support legacy objects
- Force you to have clear easy to use contracts
No comments:
Post a Comment