Skip to content

Commit 1612575

Browse files
juliocbcottamormih
authored andcommitted
[In_App_Purchase] Improve testability (flutter#2016)
This PR creates a factory for BillingClient, with that was possible to remove a secondary constructor that was used just for test. With the introduction of the factory there was the need to update some tests as it was testing states that would never be reached in the normal plugin flow (due the usage of the constructor for testing). Basically, the test cases depended on the state of BillingClient that would be null if no previous setup was executed.
1 parent 1a05843 commit 1612575

File tree

6 files changed

+79
-53
lines changed

6 files changed

+79
-53
lines changed

packages/in_app_purchase/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.1+3
2+
3+
* Android : Improved testability.
4+
15
## 0.2.1+2
26

37
* Android: Require a non-null Activity to use the `launchBillingFlow` method.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.flutter.plugins.inapppurchase;
2+
3+
import android.content.Context;
4+
import com.android.billingclient.api.BillingClient;
5+
import io.flutter.plugin.common.MethodChannel;
6+
7+
interface BillingClientFactory {
8+
BillingClient createBillingClient(Context context, MethodChannel channel);
9+
}
10+
11+
final class BillingClientFactoryImpl implements BillingClientFactory {
12+
13+
@Override
14+
public BillingClient createBillingClient(Context context, MethodChannel channel) {
15+
return BillingClient.newBuilder(context)
16+
.setListener(new PluginPurchaseListener(channel))
17+
.build();
18+
}
19+
}

packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.android.billingclient.api.ConsumeResponseListener;
2020
import com.android.billingclient.api.Purchase;
2121
import com.android.billingclient.api.PurchaseHistoryResponseListener;
22-
import com.android.billingclient.api.PurchasesUpdatedListener;
2322
import com.android.billingclient.api.SkuDetails;
2423
import com.android.billingclient.api.SkuDetailsParams;
2524
import com.android.billingclient.api.SkuDetailsResponseListener;
@@ -37,6 +36,7 @@
3736
public class InAppPurchasePlugin implements MethodCallHandler {
3837
private static final String TAG = "InAppPurchasePlugin";
3938
private @Nullable BillingClient billingClient;
39+
private final BillingClientFactory factory;
4040
private final Registrar registrar;
4141
private final Context applicationContext;
4242
private final MethodChannel channel;
@@ -69,12 +69,17 @@ static final class MethodNames {
6969
public static void registerWith(Registrar registrar) {
7070
final MethodChannel channel =
7171
new MethodChannel(registrar.messenger(), "plugins.flutter.io/in_app_purchase");
72-
channel.setMethodCallHandler(new InAppPurchasePlugin(registrar, channel));
72+
73+
final BillingClientFactory factory = new BillingClientFactoryImpl();
74+
final InAppPurchasePlugin plugin = new InAppPurchasePlugin(factory, registrar, channel);
75+
channel.setMethodCallHandler(plugin);
7376
}
7477

75-
public InAppPurchasePlugin(Registrar registrar, MethodChannel channel) {
78+
public InAppPurchasePlugin(
79+
BillingClientFactory factory, Registrar registrar, MethodChannel channel) {
7680
this.applicationContext = registrar.context();
7781
this.registrar = registrar;
82+
this.factory = factory;
7883
this.channel = channel;
7984
}
8085

@@ -112,18 +117,9 @@ public void onMethodCall(MethodCall call, Result result) {
112117
}
113118
}
114119

115-
@VisibleForTesting
116-
/*package*/ InAppPurchasePlugin(
117-
Registrar registrar, @Nullable BillingClient billingClient, MethodChannel channel) {
118-
this.billingClient = billingClient;
119-
this.channel = channel;
120-
this.applicationContext = registrar.context();
121-
this.registrar = registrar;
122-
}
123-
124120
private void startConnection(final int handle, final Result result) {
125121
if (billingClient == null) {
126-
billingClient = buildBillingClient(applicationContext, channel);
122+
billingClient = factory.createBillingClient(applicationContext, channel);
127123
}
128124

129125
billingClient.startConnection(
@@ -282,27 +278,4 @@ private boolean billingClientError(Result result) {
282278
result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null);
283279
return true;
284280
}
285-
286-
private static BillingClient buildBillingClient(Context context, MethodChannel channel) {
287-
return BillingClient.newBuilder(context)
288-
.setListener(new PluginPurchaseListener(channel))
289-
.build();
290-
}
291-
292-
@VisibleForTesting
293-
/*package*/ static class PluginPurchaseListener implements PurchasesUpdatedListener {
294-
private final MethodChannel channel;
295-
296-
PluginPurchaseListener(MethodChannel channel) {
297-
this.channel = channel;
298-
}
299-
300-
@Override
301-
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
302-
final Map<String, Object> callbackArgs = new HashMap<>();
303-
callbackArgs.put("responseCode", responseCode);
304-
callbackArgs.put("purchasesList", fromPurchasesList(purchases));
305-
channel.invokeMethod(MethodNames.ON_PURCHASES_UPDATED, callbackArgs);
306-
}
307-
}
308281
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.flutter.plugins.inapppurchase;
2+
3+
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
4+
5+
import androidx.annotation.Nullable;
6+
import com.android.billingclient.api.Purchase;
7+
import com.android.billingclient.api.PurchasesUpdatedListener;
8+
import io.flutter.plugin.common.MethodChannel;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
class PluginPurchaseListener implements PurchasesUpdatedListener {
14+
private final MethodChannel channel;
15+
16+
PluginPurchaseListener(MethodChannel channel) {
17+
this.channel = channel;
18+
}
19+
20+
@Override
21+
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
22+
final Map<String, Object> callbackArgs = new HashMap<>();
23+
callbackArgs.put("responseCode", responseCode);
24+
callbackArgs.put("purchasesList", fromPurchasesList(purchases));
25+
channel.invokeMethod(InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED, callbackArgs);
26+
}
27+
}

packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import static org.mockito.Mockito.when;
3030

3131
import android.app.Activity;
32-
import android.content.Context;
3332
import androidx.annotation.Nullable;
3433
import com.android.billingclient.api.BillingClient;
3534
import com.android.billingclient.api.BillingClient.BillingResponse;
@@ -43,12 +42,10 @@
4342
import com.android.billingclient.api.SkuDetails;
4443
import com.android.billingclient.api.SkuDetailsParams;
4544
import com.android.billingclient.api.SkuDetailsResponseListener;
46-
import io.flutter.plugin.common.BinaryMessenger;
4745
import io.flutter.plugin.common.MethodCall;
4846
import io.flutter.plugin.common.MethodChannel;
4947
import io.flutter.plugin.common.MethodChannel.Result;
5048
import io.flutter.plugin.common.PluginRegistry;
51-
import io.flutter.plugins.inapppurchase.InAppPurchasePlugin.PluginPurchaseListener;
5249
import java.util.HashMap;
5350
import java.util.List;
5451
import java.util.Map;
@@ -60,20 +57,19 @@
6057
import org.mockito.Spy;
6158

6259
public class InAppPurchasePluginTest {
63-
InAppPurchasePlugin plugin;
60+
private InAppPurchasePlugin plugin;
6461
@Mock BillingClient mockBillingClient;
6562
@Mock MethodChannel mockMethodChannel;
6663
@Spy Result result;
6764
@Mock PluginRegistry.Registrar registrar;
6865
@Mock Activity activity;
69-
@Mock BinaryMessenger messenger;
70-
@Mock Context context;
7166

7267
@Before
7368
public void setUp() {
7469
MockitoAnnotations.initMocks(this);
75-
when(registrar.context()).thenReturn(context);
76-
plugin = new InAppPurchasePlugin(registrar, mockBillingClient, mockMethodChannel);
70+
71+
BillingClientFactory factory = (context, channel) -> mockBillingClient;
72+
plugin = new InAppPurchasePlugin(factory, registrar, mockMethodChannel);
7773
}
7874

7975
@Test
@@ -85,6 +81,7 @@ public void invalidMethod() {
8581

8682
@Test
8783
public void isReady_true() {
84+
mockStartConnection();
8885
MethodCall call = new MethodCall(IS_READY, null);
8986
when(mockBillingClient.isReady()).thenReturn(true);
9087
plugin.onMethodCall(call, result);
@@ -93,6 +90,7 @@ public void isReady_true() {
9390

9491
@Test
9592
public void isReady_false() {
93+
mockStartConnection();
9694
MethodCall call = new MethodCall(IS_READY, null);
9795
when(mockBillingClient.isReady()).thenReturn(false);
9896
plugin.onMethodCall(call, result);
@@ -113,14 +111,7 @@ public void isReady_clientDisconnected() {
113111

114112
@Test
115113
public void startConnection() {
116-
Map<String, Integer> arguments = new HashMap<>();
117-
arguments.put("handle", 1);
118-
MethodCall call = new MethodCall(START_CONNECTION, arguments);
119-
ArgumentCaptor<BillingClientStateListener> captor =
120-
ArgumentCaptor.forClass(BillingClientStateListener.class);
121-
doNothing().when(mockBillingClient).startConnection(captor.capture());
122-
123-
plugin.onMethodCall(call, result);
114+
ArgumentCaptor<BillingClientStateListener> captor = mockStartConnection();
124115
verify(result, never()).success(any());
125116
captor.getValue().onBillingSetupFinished(100);
126117

@@ -452,6 +443,18 @@ public void consumeAsync() {
452443
verify(result, times(1)).success(responseCode);
453444
}
454445

446+
private ArgumentCaptor<BillingClientStateListener> mockStartConnection() {
447+
Map<String, Integer> arguments = new HashMap<>();
448+
arguments.put("handle", 1);
449+
MethodCall call = new MethodCall(START_CONNECTION, arguments);
450+
ArgumentCaptor<BillingClientStateListener> captor =
451+
ArgumentCaptor.forClass(BillingClientStateListener.class);
452+
doNothing().when(mockBillingClient).startConnection(captor.capture());
453+
454+
plugin.onMethodCall(call, result);
455+
return captor;
456+
}
457+
455458
private void establishConnectedBillingClient(
456459
@Nullable Map<String, Integer> arguments, @Nullable Result result) {
457460
if (arguments == null) {

packages/in_app_purchase/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase
22
description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play.
33
author: Flutter Team <[email protected]>
44
homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase
5-
version: 0.2.1+2
5+
version: 0.2.1+3
66

77

88
dependencies:

0 commit comments

Comments
 (0)