summaryrefslogtreecommitdiffstats
path: root/test-runner/src/android/test/ServiceTestCase.java
blob: fcb9d55e8360f69a5e3c9481161e4e2a8a79bd8d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.test;

import android.app.Application;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.test.mock.MockApplication;

import java.lang.reflect.Field;
import java.util.Random;

/**
 * This test case provides a framework in which you can test Service classes in
 * a controlled environment.  It provides basic support for the lifecycle of a
 * Service, and hooks by which you can inject various dependencies and control
 * the environment in which your Service is tested.
 *
 * <p><b>Lifecycle Support.</b>
 * Every Service is designed to be accessed within a specific sequence of
 * calls.  <insert link to Service lifecycle doc here>. 
 * In order to support the lifecycle of a Service, this test case will make the
 * following calls at the following times.
 *
 * <ul><li>The test case will not call onCreate() until your test calls 
 * {@link #startService} or {@link #bindService}.  This gives you a chance
 * to set up or adjust any additional framework or test logic before
 * onCreate().</li>
 * <li>When your test calls {@link #startService} or {@link #bindService}
 * the test case will call onCreate(), and then call the corresponding entry point in your service.
 * It will record any parameters or other support values necessary to support the lifecycle.</li>
 * <li>After your test completes, the test case {@link #tearDown} function is
 * automatically called, and it will stop and destroy your service with the appropriate
 * calls (depending on how your test invoked the service.)</li>
 * </ul>
 * 
 * <p><b>Dependency Injection.</b>
 * Every service has two inherent dependencies, the {@link android.content.Context Context} in
 * which it runs, and the {@link android.app.Application Application} with which it is associated.
 * This framework allows you to inject modified, mock, or isolated replacements for these 
 * dependencies, and thus perform a true unit test.
 * 
 * <p>If simply run your tests as-is, your Service will be injected with a fully-functional
 * Context, and a generic {@link android.test.mock.MockApplication MockApplication} object.
 * You can create and inject alternatives to either of these by calling 
 * {@link AndroidTestCase#setContext(Context) setContext()} or 
 * {@link #setApplication setApplication()}.  You must do this <i>before</i> calling
 * startService() or bindService().  The test framework provides a
 * number of alternatives for Context, including {link android.test.mock.MockContext MockContext}, 
 * {@link android.test.RenamingDelegatingContext RenamingDelegatingContext}, and 
 * {@link android.content.ContextWrapper ContextWrapper}.
 */
public abstract class ServiceTestCase<T extends Service> extends AndroidTestCase {

    Class<T> mServiceClass;

    private Context mSystemContext;
    private Application mApplication;

    public ServiceTestCase(Class<T> serviceClass) {
        mServiceClass = serviceClass;
    }

    private T mService;
    private boolean mServiceAttached = false;
    private boolean mServiceCreated = false;
    private boolean mServiceStarted = false;
    private boolean mServiceBound = false;
    private Intent mServiceIntent = null;
    private int mServiceId;

    /**
     * @return Returns the actual service under test.
     */
    public T getService() {
        return mService;
    }

    /**
     * This will do the work to instantiate the Service under test.  After this, your test 
     * code must also start and stop the service.
     */
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        
        // get the real context, before the individual tests have a chance to muck with it
        mSystemContext = getContext();

    }
    
    /**
     * Create the service under test and attach all injected dependencies (Context, Application) to
     * it.  This will be called automatically by {@link #startService} or by {@link #bindService}.
     * If you wish to call {@link AndroidTestCase#setContext(Context) setContext()} or 
     * {@link #setApplication setApplication()}, you must do so  before calling this function.
     */
    protected void setupService() {
        mService = null;
        try {
            mService = mServiceClass.newInstance();
        } catch (Exception e) {
            assertNotNull(mService);
        }
        if (getApplication() == null) {
            setApplication(new MockApplication());
        }
        mService.attach(
                getContext(),
                null,               // ActivityThread not actually used in Service
                mServiceClass.getName(),
                null,               // token not needed when not talking with the activity manager
                getApplication(),
                null                // mocked services don't talk with the activity manager
                );
        
        assertNotNull(mService);
        
        mServiceId = new Random().nextInt();
        mServiceAttached = true;
    }
    
    /**
     * Start the service under test, in the same way as if it was started by
     * {@link android.content.Context#startService Context.startService()}, providing the 
     * arguments it supplied.  If you use this method to start the service, it will automatically
     * be stopped by {@link #tearDown}.
     *  
     * @param intent The Intent as if supplied to {@link android.content.Context#startService}.
     */
    protected void startService(Intent intent) {
        assertFalse(mServiceStarted);
        assertFalse(mServiceBound);
        
        if (!mServiceAttached) {
            setupService();
        }
        assertNotNull(mService);
        
        if (!mServiceCreated) {
            mService.onCreate();
            mServiceCreated = true;
        }
        mService.onStart(intent, mServiceId);
        
        mServiceStarted = true;
    }
    
    /**
     * Start the service under test, in the same way as if it was started by
     * {@link android.content.Context#bindService Context.bindService()}, providing the 
     * arguments it supplied.
     *  
     * Return the communication channel to the service.  May return null if 
     * clients can not bind to the service.  The returned
     * {@link android.os.IBinder} is usually for a complex interface
     * that has been <a href="{@docRoot}guide/developing/tools/aidl.html">described using
     * aidl</a>. 
     * 
     * Note:  In order to test with this interface, your service must implement a getService()
     * method, as shown in samples.ApiDemos.app.LocalService.

     * @param intent The Intent as if supplied to {@link android.content.Context#bindService}.
     * 
     * @return Return an IBinder for making further calls into the Service.
     */
    protected IBinder bindService(Intent intent) {
        assertFalse(mServiceStarted);
        assertFalse(mServiceBound);
        
        if (!mServiceAttached) {
            setupService();
        }
        assertNotNull(mService);
        
        if (!mServiceCreated) {
            mService.onCreate();
            mServiceCreated = true;
        }
        // no extras are expected by unbind
        mServiceIntent = intent.cloneFilter();
        IBinder result = mService.onBind(intent);
        
        mServiceBound = true;
        return result;
    }
    
    /**
     * This will make the necessary calls to stop (or unbind) the Service under test, and
     * call onDestroy().  Ordinarily this will be called automatically (by {@link #tearDown}, but
     * you can call it directly from your test in order to check for proper shutdown behaviors.
     */
    protected void shutdownService() {
        if (mServiceStarted) {
            mService.stopSelf();
            mServiceStarted = false;
        } else if (mServiceBound) {
            mService.onUnbind(mServiceIntent);
            mServiceBound = false;
        }
        if (mServiceCreated) {
            mService.onDestroy();
        }
    }
    
    /**
     * Shuts down the Service under test.  Also makes sure all resources are cleaned up and 
     * garbage collected before moving on to the next
     * test.  Subclasses that override this method should make sure they call super.tearDown()
     * at the end of the overriding method.
     * 
     * @throws Exception
     */
    @Override
    protected void tearDown() throws Exception {
        shutdownService();
        mService = null;

        // Scrub out members - protects against memory leaks in the case where someone 
        // creates a non-static inner class (thus referencing the test case) and gives it to
        // someone else to hold onto
        scrubClass(ServiceTestCase.class);

        super.tearDown();
    }
    
    /**
     * Set the application for use during the test.  If your test does not call this function,
     * a new {@link android.test.mock.MockApplication MockApplication} object will be generated.
     * 
     * @param application The Application object that will be injected into the Service under test.
     */
    public void setApplication(Application application) {
        mApplication = application;
    }

    /**
     * Return the Application object being used by the Service under test.
     * 
     * @return Returns the application object.
     * 
     * @see #setApplication
     */
    public Application getApplication() {
        return mApplication;
    }
    
    /**
     * Return a real (not mocked or instrumented) system Context that can be used when generating
     * Mock or other Context objects for your Service under test.
     * 
     * @return Returns a reference to a normal Context.
     */
    public Context getSystemContext() {
        return mSystemContext;
    }

    public void testServiceTestCaseSetUpProperly() throws Exception {
        setupService();
        assertNotNull("service should be launched successfully", mService);
    }
}